3 Commits

Author SHA1 Message Date
Kevin BATAILLE
c226adc36c feat: implement flash messages system with auto-dismiss notifications
- Add flash message helper and styles for consistent notifications
- Replace Devise error messages with flash-based notifications
- Add dashboard page with event statistics
- Configure SMTP settings for development and production
- Update authentication controllers to use flash messages
- Add JavaScript controller for auto-dismiss functionality
2025-08-26 18:29:56 +02:00
Kevin BATAILLE
0879b3c924 fix(test): Slug was missing in ticket tests 2025-08-26 17:24:20 +02:00
Kevin BATAILLE
884c6a8262 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>
2025-08-26 17:17:50 +02:00
51 changed files with 1099 additions and 249 deletions

64
.editorconfig Normal file
View 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

View File

@@ -29,6 +29,15 @@ SMTP_PORT=1025
SMTP_AUTHENTICATION=plain SMTP_AUTHENTICATION=plain
SMTP_ENABLE_STARTTLS=false SMTP_ENABLE_STARTTLS=false
# Production SMTP Configuration (set these in .env.production)
# SMTP_ADDRESS=smtp.example.com
# SMTP_PORT=587
# SMTP_USERNAME=your_smtp_username
# SMTP_PASSWORD=your_smtp_password
# SMTP_AUTHENTICATION=plain
# SMTP_DOMAIN=example.com
# SMTP_STARTTLS=true
# Application variables # Application variables
STRIPE_API_KEY=1337 STRIPE_API_KEY=1337

View File

@@ -74,5 +74,6 @@ gem "devise", "~> 4.9"
# Pagination gem # Pagination gem
gem "kaminari", "~> 1.2" gem "kaminari", "~> 1.2"
gem "kaminari-tailwind", "~> 0.1.0"
# gem "net-pop", "~> 0.1.2" # gem "net-pop", "~> 0.1.2"

View File

@@ -159,6 +159,7 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.2) kaminari-core (= 1.2.2)
kaminari-core (1.2.2) kaminari-core (1.2.2)
kaminari-tailwind (0.1.0)
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
lint_roller (1.1.0) lint_roller (1.1.0)
logger (1.7.0) logger (1.7.0)
@@ -399,6 +400,7 @@ DEPENDENCIES
jsbundling-rails jsbundling-rails
kamal kamal
kaminari (~> 1.2) kaminari (~> 1.2)
kaminari-tailwind (~> 0.1.0)
minitest-reporters (~> 1.7) minitest-reporters (~> 1.7)
mysql2 (~> 0.5) mysql2 (~> 0.5)
propshaft propshaft

View File

@@ -1,3 +1,3 @@
web: env RUBY_DEBUG_OPEN=true bin/rails server web: env RUBY_DEBUG_OPEN=true bin/rails server
js: yarn build --watch js: yarn build:dev --watch
css: yarn build:css --watch css: yarn build:css --watch

View File

@@ -3,6 +3,9 @@
/* Import Tailwind using PostCSS */ /* Import Tailwind using PostCSS */
@import "tailwindcss"; @import "tailwindcss";
/* Import flash message styles */
@import "components/flash";
/** Default text color */ /** Default text color */
body { body {
color: #555555; color: #555555;

View File

@@ -0,0 +1,39 @@
/* Flash Messages - Theme Integration */
.flash-message {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-4;
}
/* Base styles for all flash messages */
.flash-message .flex {
@apply rounded-md p-4 border shadow-md;
}
/* Success message styles */
.flash-message-success {
@apply bg-green-50 border-green-100 text-green-800;
}
/* Error message styles */
.flash-message-error {
@apply bg-red-50 border-red-100 text-red-800;
}
/* Warning message styles */
.flash-message-warning {
@apply bg-yellow-50 border-yellow-100 text-yellow-800;
}
/* Info message styles */
.flash-message-info {
@apply bg-blue-50 border-blue-100 text-blue-800;
}
/* Notice message styles */
.flash-message-notice {
@apply bg-purple-50 border-purple-100 text-purple-800;
}
/* Alert message styles */
.flash-message-alert {
@apply bg-red-50 border-red-100 text-red-800;
}

View File

@@ -23,9 +23,11 @@ class Authentications::PasswordsController < Devise::PasswordsController
# protected # protected
# def after_resetting_password_path_for(resource) # Override to set a flash message on successful password reset
# super(resource) def after_resetting_password_path_for(resource)
# end flash[:notice] = "Votre mot de passe a été changé avec succès. Vous êtes maintenant connecté."
super(resource)
end
# The path used after sending reset password instructions # The path used after sending reset password instructions
# def after_sending_reset_password_instructions_path_for(resource_name) # def after_sending_reset_password_instructions_path_for(resource_name)

View File

@@ -1,8 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Authentications::RegistrationsController < Devise::RegistrationsController class Authentications::RegistrationsController < Devise::RegistrationsController
# before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [ :create ]
# before_action :configure_account_update_params, only: [:update] before_action :configure_account_update_params, only: [ :update ]
# GET /resource/sign_up # GET /resource/sign_up
# def new # def new
@@ -41,14 +41,14 @@ class Authentications::RegistrationsController < Devise::RegistrationsController
# protected # protected
# If you have extra params to permit, append them to the sanitizer. # If you have extra params to permit, append them to the sanitizer.
# def configure_sign_up_params def configure_sign_up_params
# devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute]) devise_parameter_sanitizer.permit(:sign_up, keys: [ :last_name, :first_name ])
# end end
# If you have extra params to permit, append them to the sanitizer. # If you have extra params to permit, append them to the sanitizer.
# def configure_account_update_params def configure_account_update_params
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute]) devise_parameter_sanitizer.permit(:account_update, keys: [ :last_name, :first_name ])
# end end
# The path used after sign up. # The path used after sign up.
# def after_sign_up_path_for(resource) # def after_sign_up_path_for(resource)

View File

@@ -9,9 +9,10 @@ class Authentications::SessionsController < Devise::SessionsController
# end # end
# POST /resource/sign_in # POST /resource/sign_in
# def create def create
# super super
# end flash[:notice] = "Connexion réussie !" if resource.persisted?
end
# DELETE /resource/sign_out # DELETE /resource/sign_out
# def destroy # def destroy

View File

@@ -3,17 +3,26 @@
class PagesController < ApplicationController class PagesController < ApplicationController
# Skip authentication for public pages # Skip authentication for public pages
# skip_before_action :authenticate_user!, only: [ :home ] # skip_before_action :authenticate_user!, only: [ :home ]
before_action :authenticate_user!, only: [ :dashboard ]
# Homepage showing featured parties # Homepage showing featured parties
def home def home
# @parties = Party.published.featured.limit(3) # @parties = Party.published.featured.limit(3)
@parties = Party.where(state: :published).order(created_at: :desc) @parties = Party.where(state: :published).order(created_at: :desc)
puts @parties
if user_signed_in?
return redirect_to(dashboard_path)
end
end end
# User dashboard showing personalized content # User dashboard showing personalized content
# Accessible only to authenticated users # Accessible only to authenticated users
def dashboard def dashboard
@available_parties = Party.published.count
@events_this_week = Party.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
@today_parties = Party.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
@tomorrow_parties = Party.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
@other_parties = Party.published.upcoming.where.not("DATE(start_time) IN (?)", [Date.current, Date.current + 1]).order(start_time: :asc).page(params[:page])
end end
# Events page showing all published parties with pagination # Events page showing all published parties with pagination

View File

@@ -1,5 +1,9 @@
module ApplicationHelper module ApplicationHelper
# Convert prince from cents to float
def format_price(cents) def format_price(cents)
(cents.to_f / 100).round(2) (cents.to_f / 100).round(2)
end end
# Include flash message helpers
include FlashMessagesHelper
end end

View File

@@ -0,0 +1,34 @@
module FlashMessagesHelper
def flash_class(type)
case type.to_s
when 'notice' then 'flash-message-success'
when 'success' then 'flash-message-success'
when 'error' then 'flash-message-error'
when 'alert' then 'flash-message-error'
when 'warning' then 'flash-message-warning'
when 'info' then 'flash-message-info'
else "flash-message-#{type}"
end
end
def flash_icon(type)
case type.to_s
when 'notice', 'success'
content_tag :svg, class: "h-5 w-5 text-green-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z", "clip-rule": "evenodd"
end
when 'error', 'alert'
content_tag :svg, class: "h-5 w-5 text-red-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z", "clip-rule": "evenodd"
end
when 'warning'
content_tag :svg, class: "h-5 w-5 text-yellow-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z", "clip-rule": "evenodd"
end
else
content_tag :svg, class: "h-5 w-5 text-blue-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", "clip-rule": "evenodd"
end
end
end
end

View File

@@ -0,0 +1,27 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["message"]
connect() {
console.log("FlashMessageController mounted", this.element);
// Auto-dismiss after 5 seconds
this.timeout = setTimeout(() => {
this.close()
}, 5000)
}
disconnect() {
if (this.timeout) {
clearTimeout(this.timeout)
}
}
close() {
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
setTimeout(() => {
this.element.remove()
}, 300)
}
}

View File

@@ -4,8 +4,13 @@
import { application } from "./application" import { application } from "./application"
import ShadcnTestController from "./shadcn_test_controller" import LogoutController from "./logout_controller"
import FlashMessage from "./flash_message_controller"
import CounterController from "./counter_controller" import CounterController from "./counter_controller"
import ShadcnTestController from "./shadcn_test_controller"
application.register("shadcn-test", ShadcnTestController) application.register("logout", LogoutController) // Allow logout using js
application.register("counter", CounterController) application.register("flash-message", FlashMessage) // Dismiss notification after 5 secondes
application.register("counter", CounterController) // Simple counter for homepage
application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn

View File

@@ -1,4 +1,3 @@
// app/javascript/controllers/logout_controller.js
import { Controller } from "@hotwired/stimulus"; import { Controller } from "@hotwired/stimulus";
export default class extends Controller { export default class extends Controller {
@@ -7,14 +6,13 @@ export default class extends Controller {
}; };
connect() { connect() {
// Optional: Add confirmation message // Display a message when the controller is mounted
//console.log("Hello LogoutController, Stimulus!", this.element); console.log("LogoutController mounted", this.element);
// this.element.dataset.confirm = "Êtes-vous sûr de vouloir vous déconnecter ?";
} }
signOut(event) { signOut(event) {
event.preventDefault(); event.preventDefault();
console.log("LogoutController#signOut mounted"); console.log("User clicked on logout button.");
// Ensure user wants to disconnect with a confirmation request // Ensure user wants to disconnect with a confirmation request
// if (this.hasUrlValue && !confirm(this.element.dataset.confirm)) { return; } // 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; const csrfToken = document.querySelector("[name='csrf-token']").content;
// Define url to redirect user when action is valid // 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 // Use fetch to send logout request
fetch(url, { fetch(url, {

View File

@@ -22,4 +22,8 @@ class User < ApplicationRecord
# Relationships # Relationships
has_many :parties, dependent: :destroy has_many :parties, dependent: :destroy
has_many :tickets, 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 end

View File

@@ -9,52 +9,50 @@
<div class="shrink-0 flex items-center"> <div class="shrink-0 flex items-center">
<%= link_to Rails.application.config.app_name, "/" , class: "text-xl font-bold text-white" %> <%= link_to Rails.application.config.app_name, "/" , class: "text-xl font-bold text-white" %>
</div> </div>
<!-- Navigation Links --> <!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex items-center"> <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 "Soirées et afterworks" , parties_path,
<%= link_to "Concerts", "#", class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %> 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>
</div> </div>
<!-- Authentication Links --> <!-- Authentication Links -->
<% if user_signed_in? %> <% if user_signed_in? %>
<!-- Settings Dropdown --> <!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6"> <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 class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open"> <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"> <button
<div><%= current_user.email %></div> 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"> <div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <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> </svg>
</div> </div>
</button> </button>
</div> </div>
<div x-show="open" x-transition:enter="transition ease-out duration-200"
<div x-show="open" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:enter="transition ease-out duration-200" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100"
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" 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" class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0" style="display: none;"
style="display: none;"
@click="open = false"> @click="open = false">
<div class="rounded-md ring-1 ring-purple-700 py-1 bg-purple-600"> <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 "Mon profil" , edit_user_registration_path,
<%= 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" %> 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" , "#" ,
<%= link_to "Déconnexion", destroy_user_session_path, class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
data: { <%= link_to "Déconnexion" , destroy_user_session_path, data: { controller: "logout" ,
controller: "logout", action: "click->logout#signOut" , logout_url_value: destroy_user_session_path, login_url_value:
action: "click->logout#signOut", new_user_session_path, turbo: false },
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" %> class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
</div> </div>
</div> </div>
@@ -63,56 +61,77 @@
<% else %> <% else %>
<!-- Login/Register Links --> <!-- Login/Register Links -->
<div class="hidden sm:flex sm:items-center sm:ms-6 space-x-4 items-center"> <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,
<%= 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" %> 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> </div>
<% end %> <% end %>
<!-- Hamburger --> <!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden"> <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"> <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="inline-flex" stroke-linecap="round"
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 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> </svg>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Responsive Navigation Menu --> <!-- Responsive Navigation Menu -->
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden"> <div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1 bg-purple-600"> <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 "Soirées et afterworks" , "#" ,
<%= 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" %> 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> </div>
<!-- Responsive Settings Options --> <!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-purple-700 bg-purple-600"> <div class="pt-4 pb-1 border-t border-purple-700 bg-purple-600">
<% if user_signed_in? %> <% if user_signed_in? %>
<div class="px-4"> <div class="px-4">
<div class="font-medium text-base text-white"><%= current_user.email %></div> <% if current_user.first_name %>
<div class="font-medium text-sm text-purple-200"><%= current_user.email %></div> <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>
<div class="mt-3 space-y-1"> <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 "Mon profil" , edit_user_registration_path,
<%= 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" %> 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, <%= link_to "Déconnexion" , destroy_user_session_path, data: { controller: "logout" , action: "click->logout#signOut",
data: { logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
controller: "logout", class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
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> </div>
<% else %> <% else %>
<div class="mt-3 space-y-1"> <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 "S'inscrire" , new_user_registration_path,
<%= 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" %> 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> </div>
<% end %> <% end %>
</div> </div>

View File

@@ -1,7 +1,6 @@
<h2>Resend confirmation instructions</h2> <h2>Resend confirmation instructions</h2>
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field"> <div class="field">
<%= f.label :email %><br /> <%= f.label :email %><br />

View File

@@ -1,4 +1,4 @@
<div class="min-h-screen flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8"> <div class="max-w-md w-full space-y-8">
<div> <div>
<%= link_to "/" do %> <%= link_to "/" do %>
@@ -13,7 +13,6 @@
</div> </div>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: "mt-8 space-y-6" }) do |f| %> <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: "mt-8 space-y-6" }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<%= f.hidden_field :reset_password_token %> <%= f.hidden_field :reset_password_token %>
<div class="space-y-4"> <div class="space-y-4">

View File

@@ -1,4 +1,4 @@
<div class="min-h-screen flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8"> <div class="max-w-md w-full space-y-8">
<div> <div>
<%= link_to "/" do %> <%= link_to "/" do %>
@@ -13,7 +13,6 @@
</div> </div>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: "mt-8 space-y-6" }) do |f| %> <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: "mt-8 space-y-6" }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div> <div>
<%= f.label :email, class: "block text-sm font-medium text-neutral-700" %> <%= f.label :email, class: "block text-sm font-medium text-neutral-700" %>

View File

@@ -1,4 +1,4 @@
<div class="min-h-screen flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8"> <div class="max-w-md w-full space-y-8">
<div> <div>
<%= link_to "/" do %> <%= link_to "/" do %>
@@ -16,7 +16,6 @@
</div> </div>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "mt-8 space-y-6" }) do |f| %> <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "mt-8 space-y-6" }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
@@ -56,7 +55,9 @@
</div> </div>
</div> </div>
<div class="mt-4">
<%= render "devise/shared/links" %> <%= render "devise/shared/links" %>
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@@ -1,4 +1,4 @@
<div class="min-h-screen flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="flex items-center justify-center bg-neutral-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8"> <div class="max-w-md w-full space-y-8">
<div> <div>
<%= link_to "/" do %> <%= link_to "/" do %>
@@ -16,7 +16,6 @@
</div> </div>
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "mt-8 space-y-6" }) do |f| %> <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "mt-8 space-y-6" }) do |f| %>
<%= devise_error_messages! %>
<div class="rounded-md shadow-sm -space-y-px"> <div class="rounded-md shadow-sm -space-y-px">
<div class="field"> <div class="field">

View File

@@ -1,14 +1,5 @@
<% if resource.errors.any? %> <% if resource.errors.any? %>
<div id="error_explanation" data-turbo-cache="false" class="bg-red-50 border border-red-200 rounded-md p-4 mb-4">
<h2 class="text-lg font-medium text-red-800 mb-3">
<%= I18n.t("errors.messages.not_saved",
count: resource.errors.count,
resource: resource.class.model_name.human.downcase) %>
</h2>
<ul class="list-disc list-inside space-y-1">
<% resource.errors.full_messages.each do |message| %> <% resource.errors.full_messages.each do |message| %>
<li class="text-sm text-red-700"><%= message %></li> <% flash.now[:error] = message %>
<% end %> <% end %>
</ul>
</div>
<% end %> <% end %>

View File

@@ -1,7 +1,6 @@
<h2>Resend unlock instructions</h2> <h2>Resend unlock instructions</h2>
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field"> <div class="field">
<%= f.label :email %><br /> <%= f.label :email %><br />

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

@@ -7,28 +7,30 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<%= yield :head %> <%= yield :head %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
<link rel="icon" href="/icon.png" type="image/png"> <link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml"> <link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.png"> <link rel="apple-touch-icon" href="/icon.png">
<%# Includes all stylesheet files in app/assets/stylesheets %> <%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "theme", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "theme", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
</head> </head>
<body class="h-full font-sans text-neutral-900 antialiased"> <body class="h-full font-sans text-neutral-900 antialiased">
<div class="min-h-full">
<div class="">
<%= render "components/header" %> <%= render "components/header" %>
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flash">
<%= render "shared/flash_messages" %>
</div>
<div class="yield">
<%= yield %> <%= yield %>
</div>
</main> </main>
<footer class="bg-neutral-100 text-neutral-600"> <footer class="bg-neutral-100 text-neutral-600">
@@ -36,6 +38,7 @@
<%= render "components/footer" %> <%= render "components/footer" %>
</div> </div>
</footer> </footer>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,157 @@
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Hero section with metrics -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-6">Tableau de bord</h1>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="group relative overflow-hidden rounded-2xl bg-white dark:bg-slate-800 border border-neutral-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 transition-all duration-300 p-8">
<div class="absolute inset-0 bg-gradient-to-br from-purple-100 to-indigo-100 dark:from-purple-900/20 dark:to-indigo-900/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="relative">
<div class="text-5xl md:text-3xl font-light bg-gradient-to-r from-purple-600 via-indigo-600 to-pink-600 bg-clip-text text-transparent mb-3">
<%= @available_parties %>
</div>
<p class="text-neutral-700 dark:text-neutral-300 font-mono uppercase tracking-widest text-sm font-medium">
Événements disponibles
</p>
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
</div>
</div>
<div class="group relative overflow-hidden rounded-2xl bg-white dark:bg-slate-800 border border-neutral-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 transition-all duration-300 p-8">
<div class="absolute inset-0 bg-gradient-to-br from-purple-100 to-indigo-100 dark:from-purple-900/20 dark:to-indigo-900/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="relative">
<div class="text-5xl md:text-3xl font-light bg-gradient-to-r from-purple-600 via-indigo-600 to-pink-600 bg-clip-text text-transparent mb-3">
<%= @events_this_week %>
</div>
<p class="text-neutral-700 dark:text-neutral-300 font-mono uppercase tracking-widest text-sm font-medium">
Événements aujourd'hui
</p>
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
</div>
</div>
<div class="group relative overflow-hidden rounded-2xl bg-white dark:bg-slate-800 border border-neutral-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 transition-all duration-300 p-8">
<div class="absolute inset-0 bg-gradient-to-br from-purple-100 to-indigo-100 dark:from-purple-900/20 dark:to-indigo-900/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="relative">
<div class="text-5xl md:text-3xl font-light bg-gradient-to-r from-purple-600 via-indigo-600 to-pink-600 bg-clip-text text-transparent mb-3">
<%= @events_this_week %>
</div>
<p class="text-neutral-700 dark:text-neutral-300 font-mono uppercase tracking-widest text-sm font-medium">
Événements cette semaine
</p>
<div class="mt-4 h-1 bg-gradient-to-r from-purple-500 via-indigo-500 to-pink-500 rounded-full w-0 group-hover:w-full transition-all duration-500"></div>
</div>
</div>
</div>
</div>
<!-- Today's parties -->
<div class="card hover-lift mb-8">
<div class="card-header">
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements du jour</h2>
</div>
<div class="card-body">
<% if @today_parties.any? %>
<ul class="space-y-4">
<% @today_parties.each do |party| %>
<li>
<%= link_to party_path(party.slug, party), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
<div class="flex items-center space-x-4">
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
<%= image_tag party.image, alt: party.name, class: "w-full h-full object-cover" if party.image.present? %>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200">
<%= party.name %>
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
<%= l(party.start_time, format: :short) %>
</p>
</div>
</div>
<% end %>
</li>
<% end %>
</ul>
<% else %>
<p class="text-slate-600 dark:text-slate-400">Aucune partie aujourd'hui.</p>
<% end %>
</div>
</div>
<!-- Tomorrow's parties -->
<div class="card hover-lift mb-8">
<div class="card-header">
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements de demain</h2>
</div>
<div class="card-body">
<% if @tomorrow_parties.any? %>
<ul class="space-y-4">
<% @tomorrow_parties.each do |party| %>
<li>
<%= link_to party_path(party.slug, party), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
<div class="flex items-center space-x-4">
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
<%= image_tag party.image, alt: party.name, class: "w-full h-full object-cover" if party.image.present? %>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200">
<%= party.name %>
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
<%= l(party.start_time, format: :short) %>
</p>
</div>
</div>
<% end %>
</li>
<% end %>
</ul>
<% else %>
<p class="text-slate-600 dark:text-slate-400">Aucune partie demain.</p>
<% end %>
</div>
</div>
<!-- Other upcoming parties with pagination -->
<div class="card hover-lift">
<div class="card-header">
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Autres évenements à venir</h2>
</div>
<div class="card-body">
<% if @other_parties.any? %>
<ul class="space-y-4">
<% @other_parties.each do |party| %>
<li>
<%= link_to party_path(party.slug, party), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
<div class="flex items-center space-x-4">
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
<%= image_tag party.image, alt: party.name, class: "w-full h-full object-cover" if party.image.present? %>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200">
<%= party.name %>
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
<%= l(party.start_time, format: :short) %>
</p>
</div>
</div>
<% end %>
</li>
<% end %>
</ul>
<!-- Pagination -->
<div class="mt-8">
<%= paginate @other_parties %>
</div>
<% else %>
<p class="text-slate-600 dark:text-slate-400">Aucune autre partie à venir.</p>
<% end %>
</div>
</div>
</div>

View File

@@ -243,8 +243,8 @@
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <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> <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> <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 S'inscrire gratuitement
</button> <% end %>
</div> </div>
</section> </section>

View File

@@ -0,0 +1,25 @@
<% flash.each do |type, message| %>
<% if message.present? %>
<div class="rounded-md bg-green-50 border-green-100 p-4 border <%= flash_class(type) %> animate-fade-in" data-controller="flash-message">
<div class="flex">
<div class="shrink-0">
<%= flash_icon(type) %>
</div>
<div class="ml-3 w-full">
<div class="space-y-2">
<div class="text-sm text-green-700">
<p class="text-sm font-medium"><%= message %></p>
</div>
</div>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button data-action="click->flash-message#close" class="inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
<% end %>
<% end %>

View File

@@ -0,0 +1,225 @@
# Authentication Messages Implementation Plan
## Overview
This document outlines the implementation of error/warn/info messages for login, registration, logout, password reset, and other authentication flows based on the existing purple/pink theme.
## Current State Analysis
- **Theme**: Purple/pink gradient system with neutral colors
- **Authentication**: Devise with custom controllers
- **Missing**: Flash message display system
- **Existing**: Only form validation errors are displayed
## Implementation Steps
### 1. Flash Message Component
Create a reusable flash message component that integrates with the theme.
### 2. CSS Classes for Message Types
Add theme-consistent styles for different message types:
- Success (green/purple)
- Error (red)
- Warning (yellow/orange)
- Info (blue)
### 3. JavaScript Enhancement
Add auto-dismiss functionality and animations
### 4. Integration
Update layouts and views to use the new message system
## Files to Create/Update
### A. Flash Message Partial
**File**: `app/views/shared/_flash_messages.html.erb`
```erb
<% flash.each do |type, message| %>
<% if message.present? %>
<div class="flash-message <%= flash_class(type) %> animate-fade-in" data-controller="flash-message">
<div class="flex items-start">
<div class="flex-shrink-0">
<%= flash_icon(type) %>
</div>
<div class="ml-3 flex-1">
<p class="text-sm font-medium"><%= message %></p>
</div>
<div class="ml-4 flex-shrink-0 flex">
<button data-action="click->flash-message#close" class="inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
<% end %>
<% end %>
```
### B. Flash Message Styling
**File**: `app/assets/stylesheets/components/flash.css`
```css
/* Flash Messages - Theme Integration */
.flash-message {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-4;
}
.flash-message .flash-container {
@apply rounded-lg p-4 shadow-md border;
}
.flash-message-success .flash-container {
@apply bg-gradient-to-r from-green-50 to-purple-50 border-green-200 text-green-800;
}
.flash-message-error .flash-container {
@apply bg-gradient-to-r from-red-50 to-pink-50 border-red-200 text-red-800;
}
.flash-message-warning .flash-container {
@apply bg-gradient-to-r from-yellow-50 to-orange-50 border-yellow-200 text-yellow-800;
}
.flash-message-info .flash-container {
@apply bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200 text-blue-800;
}
.flash-message-notice .flash-container {
@apply bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200 text-purple-800;
}
.flash-message-alert .flash-container {
@apply bg-gradient-to-r from-red-50 to-pink-50 border-red-200 text-red-800;
}
```
### C. Helper Methods
**File**: `app/helpers/flash_messages_helper.rb`
```ruby
module FlashMessagesHelper
def flash_class(type)
case type.to_s
when 'notice' then 'flash-message-success'
when 'success' then 'flash-message-success'
when 'error' then 'flash-message-error'
when 'alert' then 'flash-message-error'
when 'warning' then 'flash-message-warning'
when 'info' then 'flash-message-info'
else "flash-message-#{type}"
end
end
def flash_icon(type)
case type.to_s
when 'notice', 'success'
content_tag :svg, class: "h-5 w-5 text-green-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z", "clip-rule": "evenodd"
end
when 'error', 'alert'
content_tag :svg, class: "h-5 w-5 text-red-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z", "clip-rule": "evenodd"
end
when 'warning'
content_tag :svg, class: "h-5 w-5 text-yellow-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z", "clip-rule": "evenodd"
end
else
content_tag :svg, class: "h-5 w-5 text-blue-400", fill: "currentColor", viewBox: "0 0 20 20" do
content_tag :path, "", "fill-rule": "evenodd", "d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", "clip-rule": "evenodd"
end
end
end
end
```
### D. JavaScript Controller
**File**: `app/javascript/controllers/flash_message_controller.js`
```javascript
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["message"]
connect() {
// Auto-dismiss after 5 seconds
this.timeout = setTimeout(() => {
this.close()
}, 5000)
}
disconnect() {
if (this.timeout) {
clearTimeout(this.timeout)
}
}
close() {
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
setTimeout(() => {
this.element.remove()
}, 300)
}
}
```
### E. Update Application Layout
**File**: `app/views/layouts/application.html.erb` (add flash messages)
```erb
<body class="h-full font-sans text-neutral-900 antialiased">
<div class="min-h-full">
<%= render "components/header" %>
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<%= render "shared/flash_messages" %>
<%= yield %>
</main>
<%= render "components/footer" %>
</div>
</body>
```
### F. Update Authentication Views
Update all Devise views to remove the old error display and rely on flash messages.
## Testing Checklist
### Authentication Flows to Test:
1. **Registration**
- Successful registration
- Registration with validation errors
- Email confirmation
2. **Login**
- Successful login
- Invalid credentials
- Account locked/unconfirmed
3. **Password Reset**
- Request reset email
- Reset password success/failure
4. **Account Management**
- Update profile
- Change password
- Delete account
### Message Types to Verify:
- [ ] Success messages (green/purple)
- [ ] Error messages (red/pink)
- [ ] Warning messages (yellow/orange)
- [ ] Info messages (blue/purple)
## Implementation Order
1. Create CSS classes and theme integration
2. Create helper methods
3. Create partial templates
4. Add to application layout
5. Test each authentication flow
6. Add JavaScript enhancements
## Notes
- All messages use the existing purple/pink theme colors
- Responsive design for mobile/desktop
- Auto-dismiss functionality with manual close option
- Smooth animations and transitions
- Accessibility compliant with focus indicators

View File

@@ -23,5 +23,9 @@ module Aperonight
# #
# config.time_zone = "Central Time (US & Canada)" # config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras") # 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
end end

View File

@@ -34,6 +34,13 @@ Rails.application.configure do
# Don't care if the mailer can't send. # Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
# Configure mailer to use localhost:1025 for development
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "localhost",
port: 1025
}
# Make template changes take effect immediately. # Make template changes take effect immediately.
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false

View File

@@ -60,14 +60,17 @@ Rails.application.configure do
# Set host to be used by links generated in mailer templates. # Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = { host: "example.com" } config.action_mailer.default_url_options = { host: "example.com" }
# Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. # Configure SMTP settings using environment variables
# config.action_mailer.smtp_settings = { config.action_mailer.delivery_method = :smtp
# user_name: Rails.application.credentials.dig(:smtp, :user_name), config.action_mailer.smtp_settings = {
# password: Rails.application.credentials.dig(:smtp, :password), address: ENV.fetch("SMTP_ADDRESS", "smtp.example.com"),
# address: "smtp.example.com", port: ENV.fetch("SMTP_PORT", 587),
# port: 587, user_name: ENV.fetch("SMTP_USERNAME", ""),
# authentication: :plain password: ENV.fetch("SMTP_PASSWORD", ""),
# } authentication: ENV.fetch("SMTP_AUTHENTICATION", "plain"),
domain: ENV.fetch("SMTP_DOMAIN", "example.com"),
enable_starttls_auto: ENV.fetch("SMTP_STARTTLS", true)
}
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found). # the I18n.default_locale when a translation cannot be found).

46
config/locales/fr.yml Normal file
View File

@@ -0,0 +1,46 @@
fr:
views:
pagination:
first: "&laquo; Premier"
last: "Dernier &raquo;"
previous: "&lsaquo; Précédent"
next: "Suivant &rsaquo;"
truncate: "&hellip;"
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}&nbsp;-&nbsp;%{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"

View File

@@ -12,7 +12,10 @@ Rails.application.routes.draw do
# Defines the root path route ("/") # Defines the root path route ("/")
root "pages#home" root "pages#home"
# parties page # Pages
get "dashboard", to: "pages#dashboard", as: "dashboard"
# Parties
get "parties", to: "parties#index", as: "parties" get "parties", to: "parties#index", as: "parties"
get "parties/:slug.:id", to: "parties#show", as: "party" get "parties/:slug.:id", to: "parties#show", as: "party"
@@ -20,13 +23,13 @@ Rails.application.routes.draw do
# Bind devise to user # Bind devise to user
# devise_for :users # devise_for :users
devise_for :users, path: "auth", path_names: { devise_for :users, path: "auth", path_names: {
sign_up: "register", # Route for user registration sign_in: "sign_in", # Route for user login
sign_in: "login", # Route for user login sign_out: "sign_out", # Route for user logout
sign_out: "logout", # Route for user logout
password: "reset-password", # Route for changing password password: "reset-password", # Route for changing password
confirmation: "verification", # Route for account confirmation confirmation: "verification", # Route for account confirmation
unlock: "unblock", # Route for account unlock 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: { controllers: {
sessions: "authentications/sessions", # Custom controller for sessions sessions: "authentications/sessions", # Custom controller for sessions

View File

@@ -32,6 +32,9 @@ class DeviseCreateUsers < ActiveRecord::Migration[8.0]
# t.string :unlock_token # Only if unlock strategy is :email or :both # t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at # 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 t.timestamps null: false
end end

2
db/schema.rb generated
View File

@@ -69,6 +69,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
t.string "reset_password_token" t.string "reset_password_token"
t.datetime "reset_password_sent_at" t.datetime "reset_password_sent_at"
t.datetime "remember_created_at" t.datetime "remember_created_at"
t.string "last_name"
t.string "first_name"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true

View File

@@ -12,6 +12,8 @@
admin_user = User.find_or_create_by!(email: 'admin@example.com') do |u| admin_user = User.find_or_create_by!(email: 'admin@example.com') do |u|
u.password = 'password' u.password = 'password'
u.password_confirmation = 'password' u.password_confirmation = 'password'
u.last_name = nil
u.first_name = nil
end end
# Create regular users for development # 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| User.find_or_create_by!(email: "user#{i + 1}@example.com") do |u|
u.password = 'password' u.password = 'password'
u.password_confirmation = 'password' u.password_confirmation = 'password'
u.last_name = nil
u.first_name = nil
end end
end end
@@ -40,7 +44,7 @@ parties_data = [
start_time: 1.day.from_now, start_time: 1.day.from_now,
end_time: 1.day.from_now + 6.hours, end_time: 1.day.from_now + 6.hours,
featured: true, featured: true,
image: "https://images.unsplash.com/photo-1506157786151-b84b9d3d78d8?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", image: "https://fastly.picsum.photos/id/407/300/200.jpg?hmac=9EhoXMZ1QdwJue90vzxcjBg2YzsZsAWCjJ7oxOhtcU0",
user: users.first user: users.first
}, },
{ {

19
test.sh Normal file
View 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

View File

@@ -3,7 +3,11 @@
one: one:
email: user1@example.com email: user1@example.com
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %> encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
last_name: Trump
first_name: Donald
two: two:
email: user2@example.com email: user2@example.com
encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %> encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
last_name: Obama
first_name: Barack

View File

@@ -16,6 +16,7 @@ class TicketTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,
@@ -109,6 +110,7 @@ class TicketTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,
@@ -147,6 +149,7 @@ class TicketTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,
@@ -184,6 +187,7 @@ class TicketTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,
@@ -221,6 +225,7 @@ class TicketTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,

View File

@@ -90,6 +90,7 @@ class TicketTypeTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,
@@ -132,6 +133,7 @@ class TicketTypeTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,
@@ -162,6 +164,7 @@ class TicketTypeTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,
@@ -193,6 +196,7 @@ class TicketTypeTest < ActiveSupport::TestCase
party = Party.create!( party = Party.create!(
name: "Valid Party Name", name: "Valid Party Name",
slug: "valid-party-name",
description: "Valid description for the party that is long enough", description: "Valid description for the party that is long enough",
latitude: 48.8566, latitude: 48.8566,
longitude: 2.3522, longitude: 2.3522,

View File

@@ -25,4 +25,42 @@ class UserTest < ActiveSupport::TestCase
assert_equal :has_many, association.macro assert_equal :has_many, association.macro
assert_equal :destroy, association.options[:dependent] assert_equal :destroy, association.options[:dependent]
end 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 end