3 Commits

Author SHA1 Message Date
kbe
055640b73e Fix responsive header navigation for mobile devices
- Restructure header component to properly separate desktop and mobile views
- Ensure mobile menu is hidden by default and only shown when hamburger is
clicked
- Improve layout and spacing for better mobile experience
- Maintain all existing functionality for both desktop and mobile
2025-08-30 14:36:41 +02:00
kbe
a7e83d79d7 Fix responsive header navigation for mobile devices
- Restructure header component to properly separate desktop and mobile views
- Ensure mobile menu is hidden by default and only shown when hamburger is clicked
- Improve layout and spacing for better mobile experience
- Maintain all existing functionality for both desktop and mobile
2025-08-30 14:34:22 +02:00
kbe
9404f10c93 Replace Alpine.js with Stimulus controller for header navigation
- Create header_controller.js to handle mobile menu and user dropdown
- Replace Alpine.js directives with Stimulus data attributes in header component
- Add proper event handling for click outside to close menus
- Maintain all existing functionality with improved code consistency
2025-08-30 14:30:32 +02:00
4 changed files with 250 additions and 28 deletions

View File

@@ -0,0 +1,73 @@
import { Controller } from "@hotwired/stimulus"
// Controller for handling the header navigation
// Manages mobile menu toggle and user dropdown menu
export default class extends Controller {
static targets = ["mobileMenu", "mobileMenuButton", "userMenu", "userMenuButton"]
connect() {
// Initialize menu states
this.mobileMenuOpen = false
this.userMenuOpen = false
// Add click outside listener for user menu
this.clickOutsideHandler = this.handleClickOutside.bind(this)
document.addEventListener("click", this.clickOutsideHandler)
}
disconnect() {
// Clean up event listener
document.removeEventListener("click", this.clickOutsideHandler)
}
// Toggle mobile menu visibility
toggleMobileMenu() {
this.mobileMenuOpen = !this.mobileMenuOpen
this.mobileMenuTarget.classList.toggle("hidden", !this.mobileMenuOpen)
// Update button icon based on state
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
if (iconOpen && iconClose) {
iconOpen.classList.toggle("hidden", this.mobileMenuOpen)
iconClose.classList.toggle("hidden", !this.mobileMenuOpen)
}
}
// Toggle user dropdown menu visibility
toggleUserMenu() {
this.userMenuOpen = !this.userMenuOpen
if (this.hasUserMenuTarget) {
this.userMenuTarget.classList.toggle("hidden", !this.userMenuOpen)
}
}
// Close menus when clicking outside
handleClickOutside(event) {
// Close user menu if clicked outside
if (this.userMenuOpen && this.hasUserMenuTarget &&
!this.userMenuTarget.contains(event.target) &&
!this.userMenuButtonTarget.contains(event.target)) {
this.userMenuOpen = false
this.userMenuTarget.classList.add("hidden")
}
// Close mobile menu if clicked outside
if (this.mobileMenuOpen &&
!this.mobileMenuTarget.contains(event.target) &&
!this.mobileMenuButtonTarget.contains(event.target)) {
this.mobileMenuOpen = false
this.mobileMenuTarget.classList.add("hidden")
// Update button icon
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
if (iconOpen && iconClose) {
iconOpen.classList.remove("hidden")
iconClose.classList.add("hidden")
}
}
}
}

View File

@@ -2,10 +2,8 @@
// Run that command whenever you add a new controller or create them with // Run that command whenever you add a new controller or create them with
// ./bin/rails generate stimulus controllerName // ./bin/rails generate stimulus controllerName
// Import the main Stimulus application
import { application } from "./application" import { application } from "./application"
// Import all controllers
import LogoutController from "./logout_controller"; import LogoutController from "./logout_controller";
application.register("logout", LogoutController); application.register("logout", LogoutController);
@@ -18,6 +16,9 @@ application.register("flash-message", FlashMessageController);
import TicketSelectionController from "./ticket_selection_controller" import TicketSelectionController from "./ticket_selection_controller"
application.register("ticket-selection", TicketSelectionController); application.register("ticket-selection", TicketSelectionController);
import HeaderController from "./header_controller"
application.register("header", HeaderController);

View File

@@ -1,24 +1,25 @@
<header class="bg-neutral-800 border-b border-neutral-700"> <header class="bg-neutral-800 border-b border-neutral-700">
<nav x-data="{ open: false }" class="container mx-auto px-4 sm:px-6 lg:px-8"> <nav data-controller="header" class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16"> <div class="flex justify-between h-16">
<!-- Logo & Navigation --> <!-- Logo -->
<div class="flex items-center space-x-8"> <div class="flex items-center">
<%= link_to Rails.application.config.app_name, current_user ? "/dashboard" : "/", <%= link_to Rails.application.config.app_name, current_user ? "/dashboard" : "/",
class: "text-xl font-bold text-white" %> class: "text-xl font-bold text-white" %>
</div>
<div class="hidden sm:flex space-x-6"> <!-- Desktop Navigation -->
<%= link_to t("header.parties"), events_path, <div class="hidden sm:flex items-center space-x-6">
class: "text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %> <%= link_to t("header.parties"), events_path,
<%= link_to t("header.concerts"), "#", class: "text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
class: "text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %> <%= link_to t("header.concerts"), "#",
</div> class: "text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
</div> </div>
<!-- Authentication --> <!-- Authentication -->
<div class="flex items-center space-x-4"> <div class="hidden sm:flex items-center space-x-4">
<% if user_signed_in? %> <% if user_signed_in? %>
<div class="relative" x-data="{ open: false }" @click.outside="open = false"> <div class="relative" data-header-target="userMenuButton">
<button @click="open = !open" <button data-action="click->header#toggleUserMenu"
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 flex items-center space-x-2"> 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 flex items-center space-x-2">
<span><%= current_user.email.length > 20 ? current_user.email[0,20] + "..." : current_user.email %></span> <span><%= current_user.email.length > 20 ? current_user.email[0,20] + "..." : current_user.email %></span>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
@@ -26,8 +27,7 @@
</svg> </svg>
</button> </button>
<div x-show="open" x-transition <div data-header-target="userMenu" class="absolute right-0 mt-2 w-48 rounded-md shadow-lg z-50 hidden">
class="absolute right-0 mt-2 w-48 rounded-md shadow-lg z-50">
<%= link_to t("header.profile"), edit_user_registration_path, <%= link_to t("header.profile"), edit_user_registration_path,
class: "block px-4 py-2 text-sm bg-black text-gray-100 hover:bg-purple-700 first:rounded-t-md" %> class: "block px-4 py-2 text-sm bg-black text-gray-100 hover:bg-purple-700 first:rounded-t-md" %>
<%= link_to t("header.reservations"), "#", <%= link_to t("header.reservations"), "#",
@@ -39,25 +39,28 @@
</div> </div>
</div> </div>
<% else %> <% else %>
<div class="hidden sm:flex items-center space-x-4"> <%= link_to t("header.login"), new_user_session_path,
<%= link_to t("header.login"), new_user_session_path, class: "bg-black text-gray-100 hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
class: "bg-black text-gray-100 hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %> <%= link_to t("header.register"), new_user_registration_path,
<%= link_to t("header.register"), new_user_registration_path, class: "bg-purple-600 text-white font-medium py-2 px-4 rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
class: "bg-purple-600 text-white font-medium py-2 px-4 rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
</div>
<% end %> <% end %>
<!-- Mobile Menu Button --> </div>
<button @click="open = !open" class="sm:hidden p-2 rounded-md text-neutral-300 hover:text-white hover:bg-purple-700">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> <!-- Mobile menu button -->
<path :class="{ "hidden": open }" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> <div class="flex items-center sm:hidden">
<path :class="{ "hidden": !open }" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <button data-action="click->header#toggleMobileMenu" data-header-target="mobileMenuButton" class="p-2 rounded-md text-neutral-300 hover:text-white hover:bg-purple-700">
<svg data-menu-icon="open" class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg data-menu-icon="close" class="h-6 w-6 hidden" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
</div> </div>
</div> </div>
<!-- Mobile Menu --> <!-- Mobile Menu -->
<div :class="{ "block": open, "hidden": !open }" class="hidden sm:hidden"> <div data-header-target="mobileMenu" class="hidden sm:hidden">
<div class="px-2 pt-2 pb-3 space-y-1"> <div class="px-2 pt-2 pb-3 space-y-1">
<%= link_to t("header.parties"), events_path, <%= link_to t("header.parties"), events_path,
class: "block px-3 py-2 rounded-md text-base font-medium bg-black text-gray-100 hover:text-purple-200 hover:bg-purple-700" %> class: "block px-3 py-2 rounded-md text-base font-medium bg-black text-gray-100 hover:text-purple-200 hover:bg-purple-700" %>

View File

@@ -0,0 +1,145 @@
<!--
Note: The logout method is handled by logout_controller
in app/javascript/controllers/logout_controller.js
-->
<nav x-data="{ open: false }" class="bg-black border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<%= link_to "Cyanet", "test", class: "text-white" %>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<!-- Dashboard -->
<%= link_to "Espace client", "#",
class: "inline-flex
items-center px-1 pt-1 border-b-2 border-transparent
text-sm font-medium leading-5 text-gray-300
hover:text-gray-500 hover:border-gray-300 focus:outline-none
focus:text-gray-700 focus:border-gray-300 transition
duration-150 ease-in-out" %>
<!-- ./Dashboard -->
<!-- My services -->
<%= link_to "Mes services", "#",
class: "inline-flex
items-center px-1 pt-1 border-b-2 border-transparent
text-sm font-medium leading-5 text-gray-300
hover:text-gray-500 hover:border-gray-300 focus:outline-none
focus:text-gray-700 focus:border-gray-300 transition
duration-150 ease-in-out" %>
<!-- ./My services-->
<!-- My services -->
<%= link_to "Mes factures", "#",
class: "inline-flex
items-center px-1 pt-1 border-b-2 border-transparent
text-sm font-medium leading-5 text-gray-300
hover:text-gray-500 hover:border-gray-300 focus:outline-none
focus:text-gray-700 focus:border-gray-300 transition
duration-150 ease-in-out" %>
<!-- ./My services-->
<!-- Support -->
<%= link_to "Support", "#",
class: "inline-flex
items-center px-1 pt-1 border-b-2 border-transparent
text-sm font-medium leading-5 text-gray-300
hover:text-gray-500 hover:border-gray-300 focus:outline-none
focus:text-gray-700 focus:border-gray-300 transition
duration-150 ease-in-out" %>
<!-- ./Support-->
</div>
</div>
<!-- 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="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>Mon profil</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 py-1 bg-white">
<%= link_to "Profil", "#", class: "block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out" %>
<!-- Logout -->
<%= 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: "inline-block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out" %>
</div>
</div>
</div>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{ 'hidden': !open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<%= link_to "Dashboard", "test", class: "block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out" %>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">Test</div>
<div class="font-medium text-sm text-gray-500">Test</div>
</div>
<div class="mt-3 space-y-1">
<%= link_to "Profile", "test", class: "block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out" %>
<!-- Logout -->
<%= 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 pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-400 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out" %>
</div>
</div>
</div>
</nav>