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
This commit is contained in:
73
app/javascript/controllers/header_controller.js
Normal file
73
app/javascript/controllers/header_controller.js
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@
|
||||
// Run that command whenever you add a new controller or create them with
|
||||
// ./bin/rails generate stimulus controllerName
|
||||
|
||||
// Import the main Stimulus application
|
||||
import { application } from "./application"
|
||||
|
||||
// Import all controllers
|
||||
import LogoutController from "./logout_controller";
|
||||
application.register("logout", LogoutController);
|
||||
|
||||
@@ -18,6 +16,9 @@ application.register("flash-message", FlashMessageController);
|
||||
import TicketSelectionController from "./ticket_selection_controller"
|
||||
application.register("ticket-selection", TicketSelectionController);
|
||||
|
||||
import HeaderController from "./header_controller"
|
||||
application.register("header", HeaderController);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<!-- Logo & Navigation -->
|
||||
<div class="flex items-center space-x-8">
|
||||
@@ -17,8 +17,8 @@
|
||||
<!-- Authentication -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<% if user_signed_in? %>
|
||||
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||
<button @click="open = !open"
|
||||
<div class="relative" data-header-target="userMenuButton">
|
||||
<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">
|
||||
<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">
|
||||
@@ -26,8 +26,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div x-show="open" x-transition
|
||||
class="absolute right-0 mt-2 w-48 rounded-md shadow-lg z-50">
|
||||
<div data-header-target="userMenu" class="absolute right-0 mt-2 w-48 rounded-md shadow-lg z-50 hidden">
|
||||
<%= 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" %>
|
||||
<%= link_to t("header.reservations"), "#",
|
||||
@@ -47,17 +46,19 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- Mobile Menu Button -->
|
||||
<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">
|
||||
<path :class="{ "hidden": open }" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<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="sm:hidden 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<%= 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" %>
|
||||
|
||||
Reference in New Issue
Block a user