added club create and edit modal

This commit is contained in:
ghassan committed 2026-01-31 13:49:39 +03:00
1 parent a4bc983660
commit da688375e3
38 files changed
+9194 -44

No files matched your search

+5 -1
View File
@@ -7,7 +7,7 @@
<h2 class="h3 fw-bold mb-1">Club Details</h2>
<p class="text-muted mb-0">Manage your club information</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#editClubModal">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal">
<i class="bi bi-pencil me-2"></i>Edit Details
</button>
</div>
@@ -124,4 +124,8 @@
</div>
</div>
</div>
<!-- Include Club Modal in Edit Mode -->
<x-club-modal mode="edit" :club="$club" />
@endsection
@@ -0,0 +1,266 @@
@extends('layouts.admin')
@section('admin-content')
<div>
<!-- Page Header -->
<div class="mb-4">
<h1 class="h2 fw-bold mb-2">All Clubs</h1>
<p class="text-muted">Manage all clubs on the platform</p>
</div>
<!-- Search and Actions Bar -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="flex-grow-1 me-3">
<input type="text" id="clubSearch" class="form-control" placeholder="Search clubs by name, location, or description..." value="{{ $search ?? '' }}">
</div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
<i class="bi bi-plus-circle me-2"></i>Add New Club
</button>
</div>
<!-- Clubs Grid -->
@if($clubs->count() > 0)
<div class="row g-4 mb-4" id="clubsGrid">
@foreach($clubs as $club)
<div class="col-md-6 col-xl-4 club-card-wrapper"
data-club-name="{{ $club->club_name }}"
data-club-address="{{ $club->address ?? '' }}"
data-club-owner="{{ $club->owner->full_name ?? '' }}">
<div class="card border shadow-sm overflow-hidden club-card" style="border-radius: 0; cursor: pointer; transition: all 0.3s ease;">
<!-- Cover Image -->
<div class="position-relative overflow-hidden" style="height: 192px;" onclick="window.location.href='{{ route('admin.club.dashboard', $club) }}'">
@if($club->cover_image)
<img src="{{ asset('storage/' . $club->cover_image) }}" alt="{{ $club->club_name }}" loading="lazy" class="w-100 h-100 club-cover-img" style="object-fit: cover; transition: transform 0.3s ease;">
@else
<div class="w-100 h-100 d-flex align-items-center justify-content-center" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<i class="bi bi-image text-white" style="font-size: 3rem; opacity: 0.3;"></i>
</div>
@endif
<!-- Club Logo - Bottom Left -->
<div class="position-absolute" style="bottom: 8px; left: 8px;">
<div class="bg-white shadow border p-0.5" style="width: 80px; height: 80px; border-radius: 50%; border-color: rgba(0,0,0,0.1) !important;">
@if($club->logo)
<img src="{{ asset('storage/' . $club->logo) }}" alt="{{ $club->club_name }} logo" loading="lazy" class="w-100 h-100 rounded-circle" style="object-fit: contain;">
@else
<div class="w-100 h-100 rounded-circle bg-primary d-flex align-items-center justify-content-center">
<span class="text-white fw-bold fs-4">{{ substr($club->club_name, 0, 1) }}</span>
</div>
@endif
</div>
</div>
<!-- Admin Badge - Top Left -->
<div class="position-absolute" style="top: 8px; left: 8px;">
<span class="badge text-white px-3 py-1" style="background-color: rgba(147, 51, 234, 0.9); border-radius: 9999px; font-size: 0.75rem; font-weight: 600;">Admin</span>
</div>
<!-- Edit Button - Top Right -->
<div class="position-absolute" style="top: 8px; right: 8px;">
<button type="button"
class="btn btn-sm btn-light shadow-sm"
onclick="event.stopPropagation(); openClubModal('edit', {{ $club->id }})"
title="Edit Club">
<i class="bi bi-pencil"></i>
</button>
</div>
</div>
<!-- Card Body -->
<div class="p-4" style="background-color: white;" onclick="window.location.href='{{ route('admin.club.dashboard', $club) }}'">
<div class="mb-3">
<!-- Club Name -->
<h3 class="fw-semibold mb-2 club-title" style="font-size: 1.125rem; color: #1f2937; transition: color 0.3s ease;">{{ $club->club_name }}</h3>
<!-- Address -->
@if($club->address)
<div class="d-flex align-items-center text-muted" style="font-size: 0.875rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-1 flex-shrink-0">
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span class="text-truncate">{{ $club->address }}</span>
</div>
@endif
</div>
<!-- Stats Grid -->
<div class="row g-2 text-center" style="font-size: 0.75rem;">
<div class="col-4">
<div class="p-2 rounded" style="background-color: rgba(147, 51, 234, 0.05);">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-1" style="color: hsl(var(--primary));">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<p class="fw-semibold mb-0" style="color: #1f2937;">{{ $club->members_count }}</p>
<p class="text-muted mb-0">Members</p>
</div>
</div>
<div class="col-4">
<div class="p-2 rounded" style="background-color: rgba(147, 51, 234, 0.05);">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-1" style="color: hsl(var(--primary));">
<path d="M14.4 14.4 9.6 9.6"></path>
<path d="M18.657 21.485a2 2 0 1 1-2.829-2.828l-1.767 1.768a2 2 0 1 1-2.829-2.829l6.364-6.364a2 2 0 1 1 2.829 2.829l-1.768 1.767a2 2 0 1 1 2.828 2.829z"></path>
<path d="m21.5 21.5-1.4-1.4"></path>
<path d="M3.9 3.9 2.5 2.5"></path>
<path d="M6.404 12.768a2 2 0 1 1-2.829-2.829l1.768-1.767a2 2 0 1 1-2.828-2.829l2.828-2.828a2 2 0 1 1 2.829 2.828l1.767-1.768a2 2 0 1 1 2.829 2.829z"></path>
</svg>
<p class="fw-semibold mb-0" style="color: #1f2937;">{{ $club->packages_count }}</p>
<p class="text-muted mb-0">Packages</p>
</div>
</div>
<div class="col-4">
<div class="p-2 rounded" style="background-color: rgba(147, 51, 234, 0.05);">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-1" style="color: hsl(var(--primary));">
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>
</svg>
<p class="fw-semibold mb-0" style="color: #1f2937;">{{ $club->instructors_count }}</p>
<p class="text-muted mb-0">Trainers</p>
</div>
</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
<!-- Pagination -->
<div class="d-flex justify-content-center mb-4">
{{ $clubs->links() }}
</div>
@else
<div class="card border-0 shadow-sm mb-4">
<div class="card-body text-center py-5">
<i class="bi bi-building text-muted" style="font-size: 4rem;"></i>
<h5 class="mt-3 mb-2">No Clubs Found</h5>
<p class="text-muted mb-4">
@if($search)
No clubs match your search criteria.
@else
Get started by creating your first club.
@endif
</p>
@if(!$search)
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
<i class="bi bi-plus-circle me-2"></i>Add New Club
</button>
@endif
</div>
</div>
@endif
</div>
<!-- Include Club Modal -->
<x-club-modal mode="create" />
<!-- Include User Picker Modal -->
<x-user-picker-modal />
@push('styles')
<style>
.club-card {
transition: all 0.3s ease-in-out;
}
.club-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important;
}
.club-card:hover .club-cover-img {
transform: scale(1.1);
}
.club-card:hover .club-title {
color: hsl(var(--primary)) !important;
}
.club-card-wrapper {
transition: opacity 0.3s ease;
}
.club-card-wrapper.hidden {
display: none;
}
</style>
@endpush
@push('scripts')
<script>
// Real-time search filtering
document.getElementById('clubSearch').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const clubCards = document.querySelectorAll('.club-card-wrapper');
clubCards.forEach(function(card) {
const clubName = card.getAttribute('data-club-name').toLowerCase();
const clubAddress = card.getAttribute('data-club-address').toLowerCase();
const clubOwner = card.getAttribute('data-club-owner').toLowerCase();
if (clubName.includes(searchTerm) || clubAddress.includes(searchTerm) || clubOwner.includes(searchTerm)) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
});
// Open club modal
async function openClubModal(mode, clubId = null) {
const modal = document.getElementById('clubModal');
const form = document.getElementById('clubForm');
if (!modal || !form) return;
// Set mode
form.dataset.mode = mode;
form.dataset.clubId = clubId || '';
// Update modal title
const modalTitle = modal.querySelector('.modal-title');
if (modalTitle) {
modalTitle.textContent = mode === 'edit' ? 'Edit Club' : 'Create New Club';
}
// Update submit button text
const submitBtn = document.getElementById('submitBtn');
if (submitBtn) {
submitBtn.innerHTML = mode === 'edit'
? '<i class="bi bi-check-circle me-2"></i>Update Club'
: '<i class="bi bi-check-circle me-2"></i>Create Club';
}
// If edit mode, load club data
if (mode === 'edit' && clubId) {
try {
const response = await fetch(`/admin/api/clubs/${clubId}`);
if (response.ok) {
const club = await response.json();
populateFormWithClubData(club);
}
} catch (error) {
console.error('Error loading club data:', error);
}
} else {
// Reset form for create mode
form.reset();
}
}
// Populate form with club data (for edit mode)
function populateFormWithClubData(club) {
// This would populate all form fields with club data
// Implementation depends on the exact structure
console.log('Loading club data:', club);
// Example: populate basic fields
if (club.club_name) document.getElementById('club_name').value = club.club_name;
if (club.slug) document.getElementById('slug').value = club.slug;
// ... populate other fields
}
</script>
@endpush
@endsection
+102 -8
View File
@@ -13,9 +13,9 @@
<div class="flex-grow-1 me-3">
<input type="text" id="clubSearch" class="form-control" placeholder="Search clubs by name, location, or description..." value="{{ $search ?? '' }}">
</div>
<a href="{{ route('admin.platform.clubs.create') }}" class="btn btn-primary">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
<i class="bi bi-plus-circle me-2"></i>Add New Club
</a>
</button>
</div>
<!-- Clubs Grid -->
@@ -51,12 +51,22 @@
</div>
</div>
<!-- Admin Badge - Top Left -->
<div class="position-absolute" style="top: 8px; left: 8px;">
<span class="badge text-white px-3 py-1" style="background-color: rgba(147, 51, 234, 0.9); border-radius: 9999px; font-size: 0.75rem; font-weight: 600;">Admin</span>
</div>
<!-- Admin Badge - Top Left -->
<div class="position-absolute" style="top: 8px; left: 8px;">
<span class="badge text-white px-3 py-1" style="background-color: rgba(147, 51, 234, 0.9); border-radius: 9999px; font-size: 0.75rem; font-weight: 600;">Admin</span>
</div>
<!-- Edit Button - Top Right -->
<div class="position-absolute" style="top: 8px; right: 8px;">
<button type="button"
class="btn btn-sm btn-light shadow-sm"
onclick="event.stopPropagation(); openClubModal('edit', {{ $club->id }})"
title="Edit Club">
<i class="bi bi-pencil"></i>
</button>
</div>
</div>
<!-- Card Body -->
<div class="p-4" style="background-color: white;">
<div class="mb-3">
@@ -136,15 +146,18 @@
@endif
</p>
@if(!$search)
<a href="{{ route('admin.platform.clubs.create') }}" class="btn btn-primary">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clubModal" onclick="openClubModal('create')">
<i class="bi bi-plus-circle me-2"></i>Add New Club
</a>
</button>
@endif
</div>
</div>
@endif
</div>
<!-- Include Club Modal -->
<x-club-modal mode="create" />
@push('styles')
<style>
.club-card {
@@ -193,6 +206,87 @@
}
});
});
// Open club modal
async function openClubModal(mode, clubId = null) {
const modal = document.getElementById('clubModal');
const form = document.getElementById('clubForm');
if (!modal || !form) return;
// Set mode
form.dataset.mode = mode;
form.dataset.clubId = clubId || '';
// Update modal title
const modalTitle = modal.querySelector('.modal-title');
if (modalTitle) {
modalTitle.textContent = mode === 'edit' ? 'Edit Club' : 'Create New Club';
}
// Update submit button text
const submitBtn = document.getElementById('submitBtn');
if (submitBtn) {
submitBtn.innerHTML = mode === 'edit'
? '<i class="bi bi-check-circle me-2"></i>Update Club'
: '<i class="bi bi-check-circle me-2"></i>Create Club';
}
// If edit mode, load club data
if (mode === 'edit' && clubId) {
try {
const response = await fetch(`/admin/api/clubs/${clubId}`);
if (response.ok) {
const club = await response.json();
populateFormWithClubData(club);
}
} catch (error) {
console.error('Error loading club data:', error);
}
} else {
// Reset form for create mode
form.reset();
}
}
// Populate form with club data (for edit mode)
function populateFormWithClubData(club) {
console.log('Loading club data:', club);
// Populate basic fields
if (club.club_name) document.getElementById('club_name').value = club.club_name;
if (club.slug) document.getElementById('slug').value = club.slug;
if (club.slogan) document.getElementById('slogan').value = club.slogan;
if (club.description) document.getElementById('description').value = club.description;
if (club.established_date) document.getElementById('established_date').value = club.established_date;
if (club.commercial_reg_number) document.getElementById('commercial_reg_number').value = club.commercial_reg_number;
if (club.vat_reg_number) document.getElementById('vat_reg_number').value = club.vat_reg_number;
if (club.vat_percentage) document.getElementById('vat_percentage').value = club.vat_percentage;
// Populate owner
if (club.owner_user_id) document.getElementById('owner_user_id').value = club.owner_user_id;
// Populate location fields
if (club.country) document.getElementById('country').value = club.country;
if (club.timezone) document.getElementById('timezone').value = club.timezone;
if (club.currency) document.getElementById('currency').value = club.currency;
if (club.address) document.getElementById('address').value = club.address;
if (club.gps_lat) document.getElementById('gps_lat').value = club.gps_lat;
if (club.gps_long) document.getElementById('gps_long').value = club.gps_long;
// Populate contact fields
if (club.email) {
document.getElementById('email_option_custom').checked = true;
document.getElementById('email').value = club.email;
}
// Populate finance fields
if (club.enrollment_fee) document.getElementById('enrollment_fee').value = club.enrollment_fee;
if (club.status) document.getElementById('club_status').value = club.status;
if (club.public_profile_enabled !== undefined) {
document.getElementById('public_profile_enabled').checked = club.public_profile_enabled;
}
}
</script>
@endpush
@endsection
@@ -0,0 +1,698 @@
@props(['mode' => 'create', 'club' => null])
@php
$isEdit = $mode === 'edit' && $club;
$modalId = 'clubModal';
$modalTitle = $isEdit ? 'Edit Club' : 'Create New Club';
@endphp
<!-- Club Modal -->
<div class="modal fade" id="{{ $modalId }}" tabindex="-1" aria-labelledby="{{ $modalId }}Label" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content" style="border-radius: 1rem; border: none; max-height: 90vh; display: flex; flex-direction: column;">
<!-- Modal Header -->
<div class="modal-header border-0 pb-0" style="padding: 1.5rem 1.5rem 0;">
<div class="w-100">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="modal-title fw-bold mb-1" id="{{ $modalId }}Label">{{ $modalTitle }}</h4>
<p class="text-muted small mb-0">Fill in the information across all tabs</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Progress Indicator -->
<div class="d-flex align-items-center gap-2 mb-3">
<span class="badge bg-primary" id="stepIndicator">Step 1 of 5</span>
<div class="progress flex-grow-1" style="height: 6px;">
<div class="progress-bar" id="progressBar" role="progressbar" style="width: 20%;" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<!-- Tab Navigation -->
<ul class="nav nav-tabs border-0" id="clubModalTabs" role="tablist" style="gap: 0.5rem; flex-wrap: nowrap; overflow-x: auto;">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true" data-step="1">
<i class="bi bi-info-circle me-2"></i>
<span class="d-none d-md-inline">Basic Info</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="identity-tab" data-bs-toggle="tab" data-bs-target="#identity" type="button" role="tab" aria-controls="identity" aria-selected="false" data-step="2">
<i class="bi bi-palette me-2"></i>
<span class="d-none d-md-inline">Identity & Branding</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location" type="button" role="tab" aria-controls="location" aria-selected="false" data-step="3">
<i class="bi bi-geo-alt me-2"></i>
<span class="d-none d-md-inline">Location</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false" data-step="4">
<i class="bi bi-telephone me-2"></i>
<span class="d-none d-md-inline">Contact</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="finance-tab" data-bs-toggle="tab" data-bs-target="#finance" type="button" role="tab" aria-controls="finance" aria-selected="false" data-step="5">
<i class="bi bi-bank me-2"></i>
<span class="d-none d-md-inline">Finance & Settings</span>
</button>
</li>
</ul>
</div>
</div>
<!-- Modal Body (Scrollable) -->
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto; flex: 1;">
<form id="clubForm" data-mode="{{ $mode }}" data-club-id="{{ $club->id ?? '' }}">
@csrf
@if($isEdit)
@method('PUT')
@endif
<div class="tab-content" id="clubModalTabContent">
<!-- Tab 1: Basic Information -->
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
<x-club-modal.tabs.basic-info :club="$club" :mode="$mode" />
</div>
<!-- Tab 2: Identity & Branding -->
<div class="tab-pane fade" id="identity" role="tabpanel" aria-labelledby="identity-tab">
<x-club-modal.tabs.identity-branding :club="$club" :mode="$mode" />
</div>
<!-- Tab 3: Location -->
<div class="tab-pane fade" id="location" role="tabpanel" aria-labelledby="location-tab">
<x-club-modal.tabs.location :club="$club" :mode="$mode" />
</div>
<!-- Tab 4: Contact -->
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
<x-club-modal.tabs.contact :club="$club" :mode="$mode" />
</div>
<!-- Tab 5: Finance & Settings -->
<div class="tab-pane fade" id="finance" role="tabpanel" aria-labelledby="finance-tab">
<x-club-modal.tabs.finance-settings :club="$club" :mode="$mode" />
</div>
</div>
</form>
</div>
<!-- Modal Footer -->
<div class="modal-footer border-0" style="padding: 1rem 1.5rem 1.5rem; gap: 0.75rem;">
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
<i class="bi bi-arrow-left me-2"></i>Back
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="nextBtn">
Next<i class="bi bi-arrow-right ms-2"></i>
</button>
<button type="button" class="btn text-white" id="submitBtn" style="display: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<i class="bi bi-check-circle me-2"></i>{{ $isEdit ? 'Update Club' : 'Create Club' }}
</button>
</div>
</div>
</div>
</div>
@push('styles')
<style>
/* Club Modal Custom Styles */
#clubModal .nav-tabs {
border-bottom: 2px solid hsl(var(--border));
}
#clubModal .nav-tabs .nav-link {
border: none;
border-bottom: 3px solid transparent;
color: hsl(var(--muted-foreground));
font-weight: 500;
padding: 0.75rem 1rem;
transition: all 0.2s;
white-space: nowrap;
}
#clubModal .nav-tabs .nav-link:hover {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary) / 0.3);
}
#clubModal .nav-tabs .nav-link.active {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary));
background-color: transparent;
}
#clubModal .nav-tabs .nav-link i {
font-size: 1.1rem;
}
#clubModal .modal-body {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
#clubModal .modal-body::-webkit-scrollbar {
width: 8px;
}
#clubModal .modal-body::-webkit-scrollbar-track {
background: transparent;
}
#clubModal .modal-body::-webkit-scrollbar-thumb {
background-color: hsl(var(--border));
border-radius: 4px;
}
#clubModal .form-label {
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
#clubModal .form-control:focus,
#clubModal .form-select:focus {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 0.2rem hsl(var(--primary) / 0.15);
}
#clubModal .tab-pane {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ISSUE 1 FIX: Internal User Picker Overlay Styles */
.user-picker-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1060;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.user-picker-panel {
background: white;
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 600px;
width: 100%;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.user-picker-header {
padding: 1.5rem;
border-bottom: 1px solid hsl(var(--border));
display: flex;
justify-content: space-between;
align-items: start;
}
.user-picker-body {
padding: 1.5rem;
overflow-y: auto;
flex: 1;
}
.user-picker-item {
padding: 1rem;
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.user-picker-item:hover {
border-color: hsl(var(--primary));
background-color: hsl(var(--muted) / 0.3);
}
/* Responsive adjustments */
@media (max-width: 768px) {
#clubModal .modal-xl {
margin: 0.5rem;
}
#clubModal .nav-tabs .nav-link span {
display: none !important;
}
#clubModal .nav-tabs .nav-link {
padding: 0.75rem 0.5rem;
}
.user-picker-panel {
max-height: 90vh;
}
}
/* ISSUE 4 FIX: Map container styles */
#clubMap {
height: 400px;
width: 100%;
border-radius: 0.5rem;
z-index: 1;
}
/* Hide Leaflet attribution */
.leaflet-control-attribution {
display: none !important;
}
</style>
@endpush
@once
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
// ISSUE 1 FIX: Internal User Picker Functions
let allUsersData = [];
async function showUserPicker() {
const overlay = document.getElementById('userPickerOverlay');
if (overlay) {
overlay.style.display = 'flex';
// Load users
await loadUsersInternal();
// Focus on search input
document.getElementById('userSearchInputInternal')?.focus();
}
}
function hideUserPicker() {
const overlay = document.getElementById('userPickerOverlay');
if (overlay) {
overlay.style.display = 'none';
}
}
async function loadUsersInternal() {
const loadingDiv = document.getElementById('userPickerLoadingInternal');
const resultsDiv = document.getElementById('userPickerResultsInternal');
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
if (loadingDiv) loadingDiv.style.display = 'block';
if (resultsDiv) resultsDiv.innerHTML = '';
if (noResultsDiv) noResultsDiv.style.display = 'none';
try {
const response = await fetch('/admin/api/users');
if (response.ok) {
allUsersData = await response.json();
displayUsersInternal(allUsersData);
}
} catch (error) {
console.error('Error loading users:', error);
} finally {
if (loadingDiv) loadingDiv.style.display = 'none';
}
}
function displayUsersInternal(users) {
const resultsDiv = document.getElementById('userPickerResultsInternal');
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
if (!resultsDiv) return;
if (users.length === 0) {
resultsDiv.innerHTML = '';
if (noResultsDiv) noResultsDiv.style.display = 'block';
return;
}
if (noResultsDiv) noResultsDiv.style.display = 'none';
resultsDiv.innerHTML = users.map(user => `
<div class="user-picker-item" onclick="selectUserInternal(${user.id}, '${user.full_name}', '${user.email}', '${user.mobile_formatted || ''}', '${user.profile_picture || ''}')">
<div class="d-flex align-items-center gap-3">
${user.profile_picture
? `<img src="/storage/${user.profile_picture}" alt="${user.full_name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${user.full_name.charAt(0)}</div>`
}
<div class="flex-grow-1">
<div class="fw-semibold">${user.full_name}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>${user.email}
${user.mobile_formatted ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile_formatted}</span>` : ''}
</div>
</div>
</div>
</div>
`).join('');
}
function selectUserInternal(id, name, email, mobile, picture) {
// Set hidden input
const ownerInput = document.getElementById('owner_user_id');
if (ownerInput) {
ownerInput.value = id;
ownerInput.dispatchEvent(new Event('change'));
}
// Update display
const ownerDisplay = document.getElementById('ownerDisplay');
if (ownerDisplay) {
ownerDisplay.innerHTML = `
<div class="d-flex align-items-center gap-3">
${picture
? `<img src="/storage/${picture}" alt="${name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${name.charAt(0)}</div>`
}
<div class="flex-grow-1">
<div class="fw-semibold">${name}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>${email}
${mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${mobile}</span>` : ''}
</div>
</div>
</div>
`;
}
// Hide picker
hideUserPicker();
}
// Search functionality for internal user picker
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('userSearchInputInternal');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const searchTerm = this.value.toLowerCase();
const filtered = allUsersData.filter(user =>
user.full_name.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm) ||
(user.mobile_formatted && user.mobile_formatted.includes(searchTerm))
);
displayUsersInternal(filtered);
}, 300);
});
}
});
// Club Modal Controller
(function() {
const modal = document.getElementById('clubModal');
if (!modal) return;
const form = document.getElementById('clubForm');
const tabs = ['basic', 'identity', 'location', 'contact', 'finance'];
let currentTab = 0;
let draftLoaded = false; // ISSUE 2 & 5 FIX: Track if draft was loaded
let toastShown = {}; // ISSUE 5 FIX: Track shown toasts per tab
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const submitBtn = document.getElementById('submitBtn');
const stepIndicator = document.getElementById('stepIndicator');
const progressBar = document.getElementById('progressBar');
// Initialize
function init() {
updateButtons();
attachEventListeners();
// ISSUE 2 & 5 FIX: Load draft only once on modal open
if (!draftLoaded && form.dataset.mode === 'create') {
loadDraft();
draftLoaded = true;
}
}
// Update button visibility and progress
function updateButtons() {
prevBtn.style.display = currentTab === 0 ? 'none' : 'inline-block';
nextBtn.style.display = currentTab === tabs.length - 1 ? 'none' : 'inline-block';
submitBtn.style.display = currentTab === tabs.length - 1 ? 'inline-block' : 'none';
stepIndicator.textContent = `Step ${currentTab + 1} of ${tabs.length}`;
const progress = ((currentTab + 1) / tabs.length) * 100;
progressBar.style.width = progress + '%';
progressBar.setAttribute('aria-valuenow', progress);
}
// Navigate to specific tab
function goToTab(index) {
if (index < 0 || index >= tabs.length) return;
// Validate current tab before moving forward
if (index > currentTab && !validateCurrentTab()) {
return;
}
currentTab = index;
const tabId = tabs[index];
const tabButton = document.getElementById(tabId + '-tab');
if (tabButton) {
const tab = new bootstrap.Tab(tabButton);
tab.show();
}
updateButtons();
saveDraft();
}
// ISSUE 5 FIX: Validate current tab with single toast
function validateCurrentTab() {
const currentTabPane = document.getElementById(tabs[currentTab]);
if (!currentTabPane) return true;
const inputs = currentTabPane.querySelectorAll('input[required], select[required], textarea[required]');
let isValid = true;
let errorCount = 0;
inputs.forEach(input => {
// Skip file inputs for validation
if (input.type === 'file') return;
if (!input.value || (input.type === 'email' && !isValidEmail(input.value))) {
input.classList.add('is-invalid');
isValid = false;
errorCount++;
// Show inline error message
let errorDiv = input.nextElementSibling;
if (!errorDiv || !errorDiv.classList.contains('invalid-feedback')) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentNode.insertBefore(errorDiv, input.nextSibling);
}
errorDiv.textContent = input.dataset.errorMessage || 'This field is required.';
errorDiv.style.display = 'block';
} else {
input.classList.remove('is-invalid');
}
});
// ISSUE 5 FIX: Show only ONE toast per tab validation
if (!isValid && !toastShown[currentTab]) {
showToast(`Please fill in all required fields (${errorCount} field${errorCount > 1 ? 's' : ''} missing)`, 'error');
toastShown[currentTab] = true;
}
return isValid;
}
// Email validation
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Attach event listeners
function attachEventListeners() {
prevBtn.addEventListener('click', () => goToTab(currentTab - 1));
nextBtn.addEventListener('click', () => goToTab(currentTab + 1));
submitBtn.addEventListener('click', handleSubmit);
// Tab click navigation
document.querySelectorAll('#clubModalTabs button[data-bs-toggle="tab"]').forEach((button, index) => {
button.addEventListener('click', (e) => {
// Reset toast tracking for the new tab
toastShown[index] = false;
// Allow clicking on previous tabs, but validate before going forward
if (index > currentTab && !validateCurrentTab()) {
e.preventDefault();
return;
}
currentTab = index;
updateButtons();
});
});
// Clear validation on input
form.addEventListener('input', (e) => {
if (e.target.classList.contains('is-invalid')) {
e.target.classList.remove('is-invalid');
// Reset toast for this tab
toastShown[currentTab] = false;
}
});
// Save draft periodically
setInterval(saveDraft, 30000); // Every 30 seconds
}
// Handle form submission
async function handleSubmit() {
// Validate all tabs
let allValid = true;
for (let i = 0; i < tabs.length; i++) {
currentTab = i;
if (!validateCurrentTab()) {
allValid = false;
goToTab(i);
break;
}
}
if (!allValid) return;
const formData = new FormData(form);
const mode = form.dataset.mode;
const clubId = form.dataset.clubId;
// Show loading state
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
try {
const url = mode === 'edit'
? `/admin/clubs/${clubId}`
: '/admin/clubs';
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showToast(data.message || 'Club saved successfully!', 'success');
clearDraft();
// Close modal and reload page
setTimeout(() => {
bootstrap.Modal.getInstance(modal).hide();
window.location.reload();
}, 1500);
} else {
showToast(data.message || 'An error occurred', 'error');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
}
} catch (error) {
console.error('Error:', error);
showToast('An error occurred while saving', 'error');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
}
}
// Save draft to localStorage
function saveDraft() {
if (form.dataset.mode === 'create') {
const formData = new FormData(form);
const draft = {};
for (let [key, value] of formData.entries()) {
// ISSUE 2 FIX: Skip file inputs
const input = form.querySelector(`[name="${key}"]`);
if (input && input.type !== 'file') {
draft[key] = value;
}
}
localStorage.setItem('clubModalDraft', JSON.stringify(draft));
}
}
// ISSUE 2 FIX: Load draft from localStorage (skip file inputs)
function loadDraft() {
if (form.dataset.mode === 'create') {
const draft = localStorage.getItem('clubModalDraft');
if (draft) {
try {
const data = JSON.parse(draft);
Object.keys(data).forEach(key => {
const input = form.querySelector(`[name="${key}"]`);
// ISSUE 2 FIX: Never set value on file inputs
if (input && input.type !== 'file' && !input.value) {
input.value = data[key];
}
});
} catch (e) {
console.error('Error loading draft:', e);
// Don't show toast for draft loading errors
}
}
}
}
// Clear draft
function clearDraft() {
localStorage.removeItem('clubModalDraft');
}
// Show toast notification
function showToast(message, type = 'info') {
// Use your existing toast notification system
if (typeof Toast !== 'undefined') {
if (type === 'success') {
Toast.success('Success', message);
} else if (type === 'error') {
Toast.error('Error', message);
} else {
Toast.info('Info', message);
}
} else {
alert(message);
}
}
// Initialize when modal is shown
modal.addEventListener('shown.bs.modal', init);
// Reset on modal close
modal.addEventListener('hidden.bs.modal', () => {
currentTab = 0;
draftLoaded = false;
toastShown = {};
form.reset();
updateButtons();
});
})();
</script>
@endpush
@endonce
@@ -0,0 +1,472 @@
@props(['mode' => 'create', 'club' => null])
@php
$isEdit = $mode === 'edit' && $club;
$modalId = 'clubModal';
$modalTitle = $isEdit ? 'Edit Club' : 'Create New Club';
@endphp
<!-- Club Modal -->
<div class="modal fade" id="{{ $modalId }}" tabindex="-1" aria-labelledby="{{ $modalId }}Label" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content" style="border-radius: 1rem; border: none; max-height: 90vh; display: flex; flex-direction: column;">
<!-- Modal Header -->
<div class="modal-header border-0 pb-0" style="padding: 1.5rem 1.5rem 0;">
<div class="w-100">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="modal-title fw-bold mb-1" id="{{ $modalId }}Label">{{ $modalTitle }}</h4>
<p class="text-muted small mb-0">Fill in the information across all tabs</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Progress Indicator -->
<div class="d-flex align-items-center gap-2 mb-3">
<span class="badge bg-primary" id="stepIndicator">Step 1 of 5</span>
<div class="progress flex-grow-1" style="height: 6px;">
<div class="progress-bar" id="progressBar" role="progressbar" style="width: 20%;" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<!-- Tab Navigation -->
<ul class="nav nav-tabs border-0" id="clubModalTabs" role="tablist" style="gap: 0.5rem; flex-wrap: nowrap; overflow-x: auto;">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true" data-step="1">
<i class="bi bi-info-circle me-2"></i>
<span class="d-none d-md-inline">Basic Info</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="identity-tab" data-bs-toggle="tab" data-bs-target="#identity" type="button" role="tab" aria-controls="identity" aria-selected="false" data-step="2">
<i class="bi bi-palette me-2"></i>
<span class="d-none d-md-inline">Identity & Branding</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location" type="button" role="tab" aria-controls="location" aria-selected="false" data-step="3">
<i class="bi bi-geo-alt me-2"></i>
<span class="d-none d-md-inline">Location</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false" data-step="4">
<i class="bi bi-telephone me-2"></i>
<span class="d-none d-md-inline">Contact</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="finance-tab" data-bs-toggle="tab" data-bs-target="#finance" type="button" role="tab" aria-controls="finance" aria-selected="false" data-step="5">
<i class="bi bi-bank me-2"></i>
<span class="d-none d-md-inline">Finance & Settings</span>
</button>
</li>
</ul>
</div>
</div>
<!-- Modal Body (Scrollable) -->
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto; flex: 1;">
<form id="clubForm" data-mode="{{ $mode }}" data-club-id="{{ $club->id ?? '' }}">
@csrf
@if($isEdit)
@method('PUT')
@endif
<div class="tab-content" id="clubModalTabContent">
<!-- Tab 1: Basic Information -->
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
<x-club-modal.tabs.basic-info :club="$club" :mode="$mode" />
</div>
<!-- Tab 2: Identity & Branding -->
<div class="tab-pane fade" id="identity" role="tabpanel" aria-labelledby="identity-tab">
<x-club-modal.tabs.identity-branding :club="$club" :mode="$mode" />
</div>
<!-- Tab 3: Location -->
<div class="tab-pane fade" id="location" role="tabpanel" aria-labelledby="location-tab">
<x-club-modal.tabs.location :club="$club" :mode="$mode" />
</div>
<!-- Tab 4: Contact -->
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
<x-club-modal.tabs.contact :club="$club" :mode="$mode" />
</div>
<!-- Tab 5: Finance & Settings -->
<div class="tab-pane fade" id="finance" role="tabpanel" aria-labelledby="finance-tab">
<x-club-modal.tabs.finance-settings :club="$club" :mode="$mode" />
</div>
</div>
</form>
</div>
<!-- Modal Footer -->
<div class="modal-footer border-0" style="padding: 1rem 1.5rem 1.5rem; gap: 0.75rem;">
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
<i class="bi bi-arrow-left me-2"></i>Back
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="nextBtn">
Next<i class="bi bi-arrow-right ms-2"></i>
</button>
<button type="button" class="btn text-white" id="submitBtn" style="display: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<i class="bi bi-check-circle me-2"></i>{{ $isEdit ? 'Update Club' : 'Create Club' }}
</button>
</div>
</div>
</div>
</div>
@push('styles')
<style>
/* Club Modal Custom Styles */
#clubModal .nav-tabs {
border-bottom: 2px solid hsl(var(--border));
}
#clubModal .nav-tabs .nav-link {
border: none;
border-bottom: 3px solid transparent;
color: hsl(var(--muted-foreground));
font-weight: 500;
padding: 0.75rem 1rem;
transition: all 0.2s;
white-space: nowrap;
}
#clubModal .nav-tabs .nav-link:hover {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary) / 0.3);
}
#clubModal .nav-tabs .nav-link.active {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary));
background-color: transparent;
}
#clubModal .nav-tabs .nav-link i {
font-size: 1.1rem;
}
#clubModal .modal-body {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
#clubModal .modal-body::-webkit-scrollbar {
width: 8px;
}
#clubModal .modal-body::-webkit-scrollbar-track {
background: transparent;
}
#clubModal .modal-body::-webkit-scrollbar-thumb {
background-color: hsl(var(--border));
border-radius: 4px;
}
#clubModal .form-label {
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
#clubModal .form-control:focus,
#clubModal .form-select:focus {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 0.2rem hsl(var(--primary) / 0.15);
}
#clubModal .tab-pane {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
#clubModal .modal-xl {
margin: 0.5rem;
}
#clubModal .nav-tabs .nav-link span {
display: none !important;
}
#clubModal .nav-tabs .nav-link {
padding: 0.75rem 0.5rem;
}
}
</style>
@endpush
@once
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
// Club Modal Controller
(function() {
const modal = document.getElementById('clubModal');
if (!modal) return;
const form = document.getElementById('clubForm');
const tabs = ['basic', 'identity', 'location', 'contact', 'finance'];
let currentTab = 0;
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const submitBtn = document.getElementById('submitBtn');
const stepIndicator = document.getElementById('stepIndicator');
const progressBar = document.getElementById('progressBar');
// Initialize
function init() {
updateButtons();
attachEventListeners();
loadDraft();
}
// Update button visibility and progress
function updateButtons() {
prevBtn.style.display = currentTab === 0 ? 'none' : 'inline-block';
nextBtn.style.display = currentTab === tabs.length - 1 ? 'none' : 'inline-block';
submitBtn.style.display = currentTab === tabs.length - 1 ? 'inline-block' : 'none';
stepIndicator.textContent = `Step ${currentTab + 1} of ${tabs.length}`;
const progress = ((currentTab + 1) / tabs.length) * 100;
progressBar.style.width = progress + '%';
progressBar.setAttribute('aria-valuenow', progress);
}
// Navigate to specific tab
function goToTab(index) {
if (index < 0 || index >= tabs.length) return;
// Validate current tab before moving forward
if (index > currentTab && !validateCurrentTab()) {
return;
}
currentTab = index;
const tabId = tabs[index];
const tabButton = document.getElementById(tabId + '-tab');
if (tabButton) {
const tab = new bootstrap.Tab(tabButton);
tab.show();
}
updateButtons();
saveDraft();
}
// Validate current tab
function validateCurrentTab() {
const currentTabPane = document.getElementById(tabs[currentTab]);
if (!currentTabPane) return true;
const inputs = currentTabPane.querySelectorAll('input[required], select[required], textarea[required]');
let isValid = true;
inputs.forEach(input => {
if (!input.value || (input.type === 'email' && !isValidEmail(input.value))) {
input.classList.add('is-invalid');
isValid = false;
// Show error message
let errorDiv = input.nextElementSibling;
if (!errorDiv || !errorDiv.classList.contains('invalid-feedback')) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentNode.insertBefore(errorDiv, input.nextSibling);
}
errorDiv.textContent = input.dataset.errorMessage || 'This field is required.';
} else {
input.classList.remove('is-invalid');
}
});
if (!isValid) {
showToast('Please fill in all required fields', 'error');
}
return isValid;
}
// Email validation
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Attach event listeners
function attachEventListeners() {
prevBtn.addEventListener('click', () => goToTab(currentTab - 1));
nextBtn.addEventListener('click', () => goToTab(currentTab + 1));
submitBtn.addEventListener('click', handleSubmit);
// Tab click navigation
document.querySelectorAll('#clubModalTabs button[data-bs-toggle="tab"]').forEach((button, index) => {
button.addEventListener('click', (e) => {
// Allow clicking on previous tabs, but validate before going forward
if (index > currentTab && !validateCurrentTab()) {
e.preventDefault();
return;
}
currentTab = index;
updateButtons();
});
});
// Clear validation on input
form.addEventListener('input', (e) => {
if (e.target.classList.contains('is-invalid')) {
e.target.classList.remove('is-invalid');
}
});
// Save draft periodically
setInterval(saveDraft, 30000); // Every 30 seconds
}
// Handle form submission
async function handleSubmit() {
// Validate all tabs
let allValid = true;
for (let i = 0; i < tabs.length; i++) {
currentTab = i;
if (!validateCurrentTab()) {
allValid = false;
goToTab(i);
break;
}
}
if (!allValid) return;
const formData = new FormData(form);
const mode = form.dataset.mode;
const clubId = form.dataset.clubId;
// Show loading state
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
try {
const url = mode === 'edit'
? `/admin/clubs/${clubId}`
: '/admin/clubs';
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showToast(data.message || 'Club saved successfully!', 'success');
clearDraft();
// Close modal and reload page
setTimeout(() => {
bootstrap.Modal.getInstance(modal).hide();
window.location.reload();
}, 1500);
} else {
showToast(data.message || 'An error occurred', 'error');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
}
} catch (error) {
console.error('Error:', error);
showToast('An error occurred while saving', 'error');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
}
}
// Save draft to localStorage
function saveDraft() {
if (form.dataset.mode === 'create') {
const formData = new FormData(form);
const draft = {};
for (let [key, value] of formData.entries()) {
draft[key] = value;
}
localStorage.setItem('clubModalDraft', JSON.stringify(draft));
}
}
// Load draft from localStorage
function loadDraft() {
if (form.dataset.mode === 'create') {
const draft = localStorage.getItem('clubModalDraft');
if (draft) {
try {
const data = JSON.parse(draft);
Object.keys(data).forEach(key => {
const input = form.querySelector(`[name="${key}"]`);
if (input && !input.value) {
input.value = data[key];
}
});
} catch (e) {
console.error('Error loading draft:', e);
}
}
}
}
// Clear draft
function clearDraft() {
localStorage.removeItem('clubModalDraft');
}
// Show toast notification
function showToast(message, type = 'info') {
// Use your existing toast notification system
if (typeof Toast !== 'undefined') {
if (type === 'success') {
Toast.success('Success', message);
} else if (type === 'error') {
Toast.error('Error', message);
} else {
Toast.info('Info', message);
}
} else {
alert(message);
}
}
// Initialize when modal is shown
modal.addEventListener('shown.bs.modal', init);
// Reset on modal close
modal.addEventListener('hidden.bs.modal', () => {
currentTab = 0;
form.reset();
updateButtons();
});
})();
</script>
@endpush
@endonce
@@ -0,0 +1,719 @@
@props(['mode' => 'create', 'club' => null])
@php
$isEdit = $mode === 'edit' && $club;
$modalId = 'clubModal';
$modalTitle = $isEdit ? 'Edit Club' : 'Create New Club';
@endphp
<!-- Club Modal -->
<div class="modal fade" id="{{ $modalId }}" tabindex="-1" aria-labelledby="{{ $modalId }}Label" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content" style="border-radius: 1rem; border: none; max-height: 90vh; display: flex; flex-direction: column;">
<!-- Modal Header -->
<div class="modal-header border-0 pb-0" style="padding: 1.5rem 1.5rem 0;">
<div class="w-100">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="modal-title fw-bold mb-1" id="{{ $modalId }}Label">{{ $modalTitle }}</h4>
<p class="text-muted small mb-0">Fill in the information across all tabs</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Progress Indicator -->
<div class="d-flex align-items-center gap-2 mb-3">
<span class="badge bg-primary" id="stepIndicator">Step 1 of 5</span>
<div class="progress flex-grow-1" style="height: 6px;">
<div class="progress-bar" id="progressBar" role="progressbar" style="width: 20%;" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<!-- Tab Navigation -->
<ul class="nav nav-tabs border-0" id="clubModalTabs" role="tablist" style="gap: 0.5rem; flex-wrap: nowrap; overflow-x: auto;">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true" data-step="1">
<i class="bi bi-info-circle me-2"></i>
<span class="d-none d-md-inline">Basic Info</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="identity-tab" data-bs-toggle="tab" data-bs-target="#identity" type="button" role="tab" aria-controls="identity" aria-selected="false" data-step="2">
<i class="bi bi-palette me-2"></i>
<span class="d-none d-md-inline">Identity & Branding</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location" type="button" role="tab" aria-controls="location" aria-selected="false" data-step="3">
<i class="bi bi-geo-alt me-2"></i>
<span class="d-none d-md-inline">Location</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false" data-step="4">
<i class="bi bi-telephone me-2"></i>
<span class="d-none d-md-inline">Contact</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="finance-tab" data-bs-toggle="tab" data-bs-target="#finance" type="button" role="tab" aria-controls="finance" aria-selected="false" data-step="5">
<i class="bi bi-bank me-2"></i>
<span class="d-none d-md-inline">Finance & Settings</span>
</button>
</li>
</ul>
</div>
</div>
<!-- Modal Body (Scrollable) -->
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto; flex: 1;">
<form id="clubForm" data-mode="{{ $mode }}" data-club-id="{{ $club->id ?? '' }}">
@csrf
@if($isEdit)
@method('PUT')
@endif
<div class="tab-content" id="clubModalTabContent">
<!-- Tab 1: Basic Information -->
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
<x-club-modal.tabs.basic-info :club="$club" :mode="$mode" />
</div>
<!-- Tab 2: Identity & Branding -->
<div class="tab-pane fade" id="identity" role="tabpanel" aria-labelledby="identity-tab">
<x-club-modal.tabs.identity-branding :club="$club" :mode="$mode" />
</div>
<!-- Tab 3: Location -->
<div class="tab-pane fade" id="location" role="tabpanel" aria-labelledby="location-tab">
<x-club-modal.tabs.location :club="$club" :mode="$mode" />
</div>
<!-- Tab 4: Contact -->
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
<x-club-modal.tabs.contact :club="$club" :mode="$mode" />
</div>
<!-- Tab 5: Finance & Settings -->
<div class="tab-pane fade" id="finance" role="tabpanel" aria-labelledby="finance-tab">
<x-club-modal.tabs.finance-settings :club="$club" :mode="$mode" />
</div>
</div>
</form>
</div>
<!-- Modal Footer -->
<div class="modal-footer border-0" style="padding: 1rem 1.5rem 1.5rem; gap: 0.75rem;">
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
<i class="bi bi-arrow-left me-2"></i>Back
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="nextBtn">
Next<i class="bi bi-arrow-right ms-2"></i>
</button>
<button type="button" class="btn text-white" id="submitBtn" style="display: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<i class="bi bi-check-circle me-2"></i>{{ $isEdit ? 'Update Club' : 'Create Club' }}
</button>
</div>
</div>
</div>
</div>
@push('styles')
<style>
/* Club Modal Custom Styles */
/* PART 3 FIX: Prevent vertical scrollbar in tabs header */
#clubModal .modal-header {
overflow-y: visible;
overflow-x: hidden;
}
#clubModal .nav-tabs {
border-bottom: 2px solid hsl(var(--border));
overflow-y: hidden; /* PART 2 FIX: No vertical scroll */
overflow-x: auto; /* Allow horizontal scroll for many tabs */
flex-wrap: nowrap;
scrollbar-width: none; /* PART 2 FIX: Hide scrollbar in Firefox */
-ms-overflow-style: none; /* PART 2 FIX: Hide scrollbar in IE/Edge */
}
/* PART 2 FIX: Hide scrollbar in Chrome/Safari */
#clubModal .nav-tabs::-webkit-scrollbar {
display: none;
}
#clubModal .nav-tabs .nav-link {
border: none;
border-bottom: 3px solid transparent;
color: hsl(var(--muted-foreground));
font-weight: 500;
padding: 0.75rem 1rem;
transition: all 0.2s;
white-space: nowrap;
flex-shrink: 0; /* Prevent tabs from shrinking */
}
#clubModal .nav-tabs .nav-link:hover {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary) / 0.3);
}
#clubModal .nav-tabs .nav-link.active {
color: hsl(var(--primary));
border-bottom-color: hsl(var(--primary));
background-color: transparent;
}
#clubModal .nav-tabs .nav-link i {
font-size: 1.1rem;
}
/* PART 3 FIX: Only modal body should have vertical scroll */
#clubModal .modal-body {
overflow-y: auto; /* Vertical scroll for content */
overflow-x: hidden; /* No horizontal scroll */
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
#clubModal .modal-body::-webkit-scrollbar {
width: 8px;
}
#clubModal .modal-body::-webkit-scrollbar-track {
background: transparent;
}
#clubModal .modal-body::-webkit-scrollbar-thumb {
background-color: hsl(var(--border));
border-radius: 4px;
}
#clubModal .form-label {
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
#clubModal .form-control:focus,
#clubModal .form-select:focus {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 0.2rem hsl(var(--primary) / 0.15);
}
#clubModal .tab-pane {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ISSUE 1 FIX: Internal User Picker Overlay Styles */
.user-picker-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1060;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.user-picker-panel {
background: white;
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 600px;
width: 100%;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.user-picker-header {
padding: 1.5rem;
border-bottom: 1px solid hsl(var(--border));
display: flex;
justify-content: space-between;
align-items: start;
}
.user-picker-body {
padding: 1.5rem;
overflow-y: auto;
flex: 1;
}
.user-picker-item {
padding: 1rem;
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.user-picker-item:hover {
border-color: hsl(var(--primary));
background-color: hsl(var(--muted) / 0.3);
}
/* Responsive adjustments */
@media (max-width: 768px) {
#clubModal .modal-xl {
margin: 0.5rem;
}
#clubModal .nav-tabs .nav-link span {
display: none !important;
}
#clubModal .nav-tabs .nav-link {
padding: 0.75rem 0.5rem;
}
.user-picker-panel {
max-height: 90vh;
}
}
/* ISSUE 4 FIX: Map container styles */
#clubMap {
height: 400px;
width: 100%;
border-radius: 0.5rem;
z-index: 1;
}
/* Hide Leaflet attribution */
.leaflet-control-attribution {
display: none !important;
}
</style>
@endpush
@once
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
// ISSUE 1 FIX: Internal User Picker Functions
let allUsersData = [];
async function showUserPicker() {
const overlay = document.getElementById('userPickerOverlay');
if (overlay) {
overlay.style.display = 'flex';
// Load users
await loadUsersInternal();
// Focus on search input
document.getElementById('userSearchInputInternal')?.focus();
}
}
function hideUserPicker() {
const overlay = document.getElementById('userPickerOverlay');
if (overlay) {
overlay.style.display = 'none';
}
}
async function loadUsersInternal() {
const loadingDiv = document.getElementById('userPickerLoadingInternal');
const resultsDiv = document.getElementById('userPickerResultsInternal');
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
if (loadingDiv) loadingDiv.style.display = 'block';
if (resultsDiv) resultsDiv.innerHTML = '';
if (noResultsDiv) noResultsDiv.style.display = 'none';
try {
const response = await fetch('/admin/api/users');
if (response.ok) {
allUsersData = await response.json();
displayUsersInternal(allUsersData);
}
} catch (error) {
console.error('Error loading users:', error);
} finally {
if (loadingDiv) loadingDiv.style.display = 'none';
}
}
function displayUsersInternal(users) {
const resultsDiv = document.getElementById('userPickerResultsInternal');
const noResultsDiv = document.getElementById('userPickerNoResultsInternal');
if (!resultsDiv) return;
if (users.length === 0) {
resultsDiv.innerHTML = '';
if (noResultsDiv) noResultsDiv.style.display = 'block';
return;
}
if (noResultsDiv) noResultsDiv.style.display = 'none';
resultsDiv.innerHTML = users.map(user => `
<div class="user-picker-item" onclick="selectUserInternal(${user.id}, '${user.full_name}', '${user.email}', '${user.mobile_formatted || ''}', '${user.profile_picture || ''}')">
<div class="d-flex align-items-center gap-3">
${user.profile_picture
? `<img src="/storage/${user.profile_picture}" alt="${user.full_name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${user.full_name.charAt(0)}</div>`
}
<div class="flex-grow-1">
<div class="fw-semibold">${user.full_name}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>${user.email}
${user.mobile_formatted ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile_formatted}</span>` : ''}
</div>
</div>
</div>
</div>
`).join('');
}
function selectUserInternal(id, name, email, mobile, picture) {
// Set hidden input
const ownerInput = document.getElementById('owner_user_id');
if (ownerInput) {
ownerInput.value = id;
ownerInput.dispatchEvent(new Event('change'));
}
// Update display
const ownerDisplay = document.getElementById('ownerDisplay');
if (ownerDisplay) {
ownerDisplay.innerHTML = `
<div class="d-flex align-items-center gap-3">
${picture
? `<img src="/storage/${picture}" alt="${name}" class="rounded-circle" style="width: 50px; height: 50px; object-fit: cover;">`
: `<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center" style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">${name.charAt(0)}</div>`
}
<div class="flex-grow-1">
<div class="fw-semibold">${name}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>${email}
${mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${mobile}</span>` : ''}
</div>
</div>
</div>
`;
}
// Hide picker
hideUserPicker();
}
// Search functionality for internal user picker
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('userSearchInputInternal');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const searchTerm = this.value.toLowerCase();
const filtered = allUsersData.filter(user =>
user.full_name.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm) ||
(user.mobile_formatted && user.mobile_formatted.includes(searchTerm))
);
displayUsersInternal(filtered);
}, 300);
});
}
});
// Club Modal Controller
(function() {
const modal = document.getElementById('clubModal');
if (!modal) return;
const form = document.getElementById('clubForm');
const tabs = ['basic', 'identity', 'location', 'contact', 'finance'];
let currentTab = 0;
let draftLoaded = false; // ISSUE 2 & 5 FIX: Track if draft was loaded
let toastShown = {}; // ISSUE 5 FIX: Track shown toasts per tab
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const submitBtn = document.getElementById('submitBtn');
const stepIndicator = document.getElementById('stepIndicator');
const progressBar = document.getElementById('progressBar');
// Initialize
function init() {
updateButtons();
attachEventListeners();
// ISSUE 2 & 5 FIX: Load draft only once on modal open
if (!draftLoaded && form.dataset.mode === 'create') {
loadDraft();
draftLoaded = true;
}
}
// Update button visibility and progress
function updateButtons() {
prevBtn.style.display = currentTab === 0 ? 'none' : 'inline-block';
nextBtn.style.display = currentTab === tabs.length - 1 ? 'none' : 'inline-block';
submitBtn.style.display = currentTab === tabs.length - 1 ? 'inline-block' : 'none';
stepIndicator.textContent = `Step ${currentTab + 1} of ${tabs.length}`;
const progress = ((currentTab + 1) / tabs.length) * 100;
progressBar.style.width = progress + '%';
progressBar.setAttribute('aria-valuenow', progress);
}
// Navigate to specific tab
function goToTab(index) {
if (index < 0 || index >= tabs.length) return;
// Validate current tab before moving forward
if (index > currentTab && !validateCurrentTab()) {
return;
}
currentTab = index;
const tabId = tabs[index];
const tabButton = document.getElementById(tabId + '-tab');
if (tabButton) {
const tab = new bootstrap.Tab(tabButton);
tab.show();
}
updateButtons();
saveDraft();
}
// ISSUE 5 FIX: Validate current tab with single toast
function validateCurrentTab() {
const currentTabPane = document.getElementById(tabs[currentTab]);
if (!currentTabPane) return true;
const inputs = currentTabPane.querySelectorAll('input[required], select[required], textarea[required]');
let isValid = true;
let errorCount = 0;
inputs.forEach(input => {
// Skip file inputs for validation
if (input.type === 'file') return;
if (!input.value || (input.type === 'email' && !isValidEmail(input.value))) {
input.classList.add('is-invalid');
isValid = false;
errorCount++;
// Show inline error message
let errorDiv = input.nextElementSibling;
if (!errorDiv || !errorDiv.classList.contains('invalid-feedback')) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentNode.insertBefore(errorDiv, input.nextSibling);
}
errorDiv.textContent = input.dataset.errorMessage || 'This field is required.';
errorDiv.style.display = 'block';
} else {
input.classList.remove('is-invalid');
}
});
// ISSUE 5 FIX: Show only ONE toast per tab validation
if (!isValid && !toastShown[currentTab]) {
showToast(`Please fill in all required fields (${errorCount} field${errorCount > 1 ? 's' : ''} missing)`, 'error');
toastShown[currentTab] = true;
}
return isValid;
}
// Email validation
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Attach event listeners
function attachEventListeners() {
prevBtn.addEventListener('click', () => goToTab(currentTab - 1));
nextBtn.addEventListener('click', () => goToTab(currentTab + 1));
submitBtn.addEventListener('click', handleSubmit);
// Tab click navigation
document.querySelectorAll('#clubModalTabs button[data-bs-toggle="tab"]').forEach((button, index) => {
button.addEventListener('click', (e) => {
// Reset toast tracking for the new tab
toastShown[index] = false;
// Allow clicking on previous tabs, but validate before going forward
if (index > currentTab && !validateCurrentTab()) {
e.preventDefault();
return;
}
currentTab = index;
updateButtons();
});
});
// Clear validation on input
form.addEventListener('input', (e) => {
if (e.target.classList.contains('is-invalid')) {
e.target.classList.remove('is-invalid');
// Reset toast for this tab
toastShown[currentTab] = false;
}
});
// Save draft periodically
setInterval(saveDraft, 30000); // Every 30 seconds
}
// Handle form submission
async function handleSubmit() {
// Validate all tabs
let allValid = true;
for (let i = 0; i < tabs.length; i++) {
currentTab = i;
if (!validateCurrentTab()) {
allValid = false;
goToTab(i);
break;
}
}
if (!allValid) return;
const formData = new FormData(form);
const mode = form.dataset.mode;
const clubId = form.dataset.clubId;
// Show loading state
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
try {
const url = mode === 'edit'
? `/admin/clubs/${clubId}`
: '/admin/clubs';
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showToast(data.message || 'Club saved successfully!', 'success');
clearDraft();
// Close modal and reload page
setTimeout(() => {
bootstrap.Modal.getInstance(modal).hide();
window.location.reload();
}, 1500);
} else {
showToast(data.message || 'An error occurred', 'error');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
}
} catch (error) {
console.error('Error:', error);
showToast('An error occurred while saving', 'error');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-circle me-2"></i>' + (mode === 'edit' ? 'Update Club' : 'Create Club');
}
}
// Save draft to localStorage
function saveDraft() {
if (form.dataset.mode === 'create') {
const formData = new FormData(form);
const draft = {};
for (let [key, value] of formData.entries()) {
// ISSUE 2 FIX: Skip file inputs
const input = form.querySelector(`[name="${key}"]`);
if (input && input.type !== 'file') {
draft[key] = value;
}
}
localStorage.setItem('clubModalDraft', JSON.stringify(draft));
}
}
// ISSUE 2 FIX: Load draft from localStorage (skip file inputs)
function loadDraft() {
if (form.dataset.mode === 'create') {
const draft = localStorage.getItem('clubModalDraft');
if (draft) {
try {
const data = JSON.parse(draft);
Object.keys(data).forEach(key => {
const input = form.querySelector(`[name="${key}"]`);
// ISSUE 2 FIX: Never set value on file inputs
if (input && input.type !== 'file' && !input.value) {
input.value = data[key];
}
});
} catch (e) {
console.error('Error loading draft:', e);
// Don't show toast for draft loading errors
}
}
}
}
// Clear draft
function clearDraft() {
localStorage.removeItem('clubModalDraft');
}
// Show toast notification
function showToast(message, type = 'info') {
// Use your existing toast notification system
if (typeof Toast !== 'undefined') {
if (type === 'success') {
Toast.success('Success', message);
} else if (type === 'error') {
Toast.error('Error', message);
} else {
Toast.info('Info', message);
}
} else {
alert(message);
}
}
// Initialize when modal is shown
modal.addEventListener('shown.bs.modal', init);
// Reset on modal close
modal.addEventListener('hidden.bs.modal', () => {
currentTab = 0;
draftLoaded = false;
toastShown = {};
form.reset();
updateButtons();
});
})();
</script>
@endpush
@endonce
@@ -0,0 +1,284 @@
@props(['club' => null, 'mode' => 'create'])
@php
$isEdit = $mode === 'edit' && $club;
@endphp
<div class="container-fluid px-0">
<h5 class="fw-bold mb-3">Basic Information</h5>
<p class="text-muted mb-4">Core details about the club</p>
<!-- Club Name -->
<div class="mb-4">
<label for="club_name" class="form-label">
Club Name <span class="text-danger">*</span>
</label>
<input type="text"
class="form-control"
id="club_name"
name="club_name"
value="{{ $club->club_name ?? old('club_name') }}"
required
data-error-message="Club name is required"
placeholder="e.g., Bahrain Taekwondo Academy">
<div class="invalid-feedback">Club name is required.</div>
</div>
<!-- Club Owner -->
<div class="mb-4">
<label class="form-label">
Club Owner <span class="text-danger">*</span>
</label>
<input type="hidden" id="owner_user_id" name="owner_user_id" value="{{ $club->owner_user_id ?? old('owner_user_id') }}" required>
<div id="ownerDisplay" class="border rounded p-3 mb-2" style="background-color: hsl(var(--muted) / 0.3);">
@if($isEdit && $club->owner)
<div class="d-flex align-items-center gap-3">
@if($club->owner->profile_picture)
<img src="{{ asset('storage/' . $club->owner->profile_picture) }}"
alt="{{ $club->owner->full_name }}"
class="rounded-circle"
style="width: 50px; height: 50px; object-fit: cover;">
@else
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">
{{ substr($club->owner->full_name, 0, 1) }}
</div>
@endif
<div class="flex-grow-1">
<div class="fw-semibold">{{ $club->owner->full_name }}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>{{ $club->owner->email }}
@if($club->owner->mobile)
<span class="ms-2"><i class="bi bi-phone me-1"></i>{{ $club->owner->mobile_formatted }}</span>
@endif
</div>
</div>
</div>
@else
<div class="text-center text-muted py-3" id="noOwnerSelected">
<i class="bi bi-person-plus fs-3 mb-2"></i>
<p class="mb-0">No owner selected</p>
</div>
@endif
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="showUserPicker()">
<i class="bi bi-search me-2"></i>Select Club Owner
</button>
<div class="invalid-feedback d-block" id="ownerError" style="display: none !important;">
Please select a club owner.
</div>
</div>
<!-- Internal User Picker Overlay (NOT a separate modal) -->
<div id="userPickerOverlay" class="user-picker-overlay" style="display: none;">
<div class="user-picker-panel">
<div class="user-picker-header">
<div>
<h5 class="fw-bold mb-1">Select Club Owner</h5>
<p class="text-muted small mb-0">Search and select a user to be the club owner</p>
</div>
<button type="button" class="btn-close" onclick="hideUserPicker()"></button>
</div>
<div class="user-picker-body">
<div class="mb-3">
<div class="input-group">
<span class="input-group-text bg-white">
<i class="bi bi-search"></i>
</span>
<input type="text"
class="form-control"
id="userSearchInputInternal"
placeholder="Search by name, email, or phone..."
autocomplete="off">
</div>
</div>
<div id="userPickerLoadingInternal" class="text-center py-5" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="text-muted mt-2">Searching users...</p>
</div>
<div id="userPickerResultsInternal" style="max-height: 400px; overflow-y: auto;"></div>
<div id="userPickerNoResultsInternal" class="text-center py-5" style="display: none;">
<i class="bi bi-person-x fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">No users found</p>
<small class="text-muted">Try a different search term</small>
</div>
</div>
</div>
</div>
<!-- Established Date -->
<div class="mb-4">
<label for="established_date" class="form-label">
Established Date
</label>
<input type="date"
class="form-control"
id="established_date"
name="established_date"
value="{{ $club->established_date ?? old('established_date') }}"
max="{{ date('Y-m-d') }}">
<small class="text-muted">When was the club founded?</small>
</div>
<!-- Slogan -->
<div class="mb-4">
<label for="slogan" class="form-label">
Slogan
</label>
<input type="text"
class="form-control"
id="slogan"
name="slogan"
value="{{ $club->slogan ?? old('slogan') }}"
placeholder="e.g., Excellence in Martial Arts"
maxlength="100">
<small class="text-muted">A short, memorable tagline (max 100 characters)</small>
</div>
<!-- Description -->
<div class="mb-4">
<label for="description" class="form-label">
Description
</label>
<textarea class="form-control"
id="description"
name="description"
rows="4"
placeholder="Describe your club, its mission, and what makes it unique..."
maxlength="1000">{{ $club->description ?? old('description') }}</textarea>
<small class="text-muted">
<span id="descriptionCount">0</span>/1000 characters
</small>
</div>
<!-- Commercial Registration -->
<div class="row mb-4">
<div class="col-md-6">
<label for="commercial_reg_number" class="form-label">
Commercial Registration Number
</label>
<input type="text"
class="form-control"
id="commercial_reg_number"
name="commercial_reg_number"
value="{{ $club->commercial_reg_number ?? old('commercial_reg_number') }}"
placeholder="e.g., CR-123456">
<small class="text-muted">Official business registration number</small>
</div>
<div class="col-md-6">
<label for="commercial_reg_file" class="form-label">
Registration Document
</label>
<input type="file"
class="form-control"
id="commercial_reg_file"
name="commercial_reg_file"
accept=".pdf,.jpg,.jpeg,.png">
<small class="text-muted">Upload registration certificate (optional)</small>
</div>
</div>
<!-- VAT Information -->
<div class="row mb-4">
<div class="col-md-6">
<label for="vat_reg_number" class="form-label">
VAT Registration Number
</label>
<input type="text"
class="form-control"
id="vat_reg_number"
name="vat_reg_number"
value="{{ $club->vat_reg_number ?? old('vat_reg_number') }}"
placeholder="e.g., VAT-123456789">
<small class="text-muted">Tax registration number (if applicable)</small>
</div>
<div class="col-md-6">
<label for="vat_percentage" class="form-label">
VAT Percentage (%)
</label>
<input type="number"
class="form-control"
id="vat_percentage"
name="vat_percentage"
value="{{ $club->vat_percentage ?? old('vat_percentage', '0') }}"
min="0"
max="100"
step="0.01"
placeholder="e.g., 5.00">
<small class="text-muted">Default VAT rate for invoices</small>
</div>
</div>
<!-- VAT Certificate Upload -->
<div class="mb-4">
<label for="vat_certificate_file" class="form-label">
VAT Certificate
</label>
<input type="file"
class="form-control"
id="vat_certificate_file"
name="vat_certificate_file"
accept=".pdf,.jpg,.jpeg,.png">
<small class="text-muted">Upload VAT registration certificate (optional)</small>
</div>
</div>
@push('scripts')
<script>
// Character counter for description
document.addEventListener('DOMContentLoaded', function() {
const descriptionTextarea = document.getElementById('description');
const descriptionCount = document.getElementById('descriptionCount');
if (descriptionTextarea && descriptionCount) {
function updateCount() {
descriptionCount.textContent = descriptionTextarea.value.length;
}
descriptionTextarea.addEventListener('input', updateCount);
updateCount(); // Initial count
}
// Auto-generate slug from club name (will be used in Identity tab)
const clubNameInput = document.getElementById('club_name');
if (clubNameInput) {
clubNameInput.addEventListener('input', function() {
const slug = this.value
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const slugInput = document.getElementById('slug');
if (slugInput && !slugInput.dataset.manuallyEdited) {
slugInput.value = slug;
// Trigger slug change event for URL preview
slugInput.dispatchEvent(new Event('input'));
}
});
}
// Validate owner selection
const ownerInput = document.getElementById('owner_user_id');
if (ownerInput) {
ownerInput.addEventListener('change', function() {
const ownerError = document.getElementById('ownerError');
if (this.value) {
ownerError.style.display = 'none';
this.classList.remove('is-invalid');
} else {
ownerError.style.display = 'block';
this.classList.add('is-invalid');
}
});
}
});
</script>
@endpush
@@ -0,0 +1,227 @@
@props(['club' => null, 'mode' => 'create'])
@php
$isEdit = $mode === 'edit' && $club;
@endphp
<div class="container-fluid px-0">
<h5 class="fw-bold mb-3">Contact Information</h5>
<p class="text-muted mb-4">Set up how members can reach your club</p>
<!-- Club Email -->
<div class="mb-4">
<label class="form-label">Club Email</label>
<!-- Toggle: Use Owner Email or Custom -->
<div class="form-check mb-3">
<input class="form-check-input"
type="radio"
name="email_option"
id="email_option_owner"
value="owner"
{{ (!$isEdit || !$club->email) ? 'checked' : '' }}>
<label class="form-check-label" for="email_option_owner">
Use Club Owner's Email
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input"
type="radio"
name="email_option"
id="email_option_custom"
value="custom"
{{ ($isEdit && $club->email) ? 'checked' : '' }}>
<label class="form-check-label" for="email_option_custom">
Use Custom Email
</label>
</div>
<!-- Owner Email Display (Read-only) -->
<div id="ownerEmailDisplay" class="border rounded p-3 mb-3" style="background-color: hsl(var(--muted) / 0.2); display: {{ (!$isEdit || !$club->email) ? 'block' : 'none' }};">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-envelope text-muted"></i>
<span id="ownerEmailText" class="text-muted">
@if($isEdit && $club->owner)
{{ $club->owner->email }}
@else
Select a club owner first
@endif
</span>
</div>
</div>
<!-- Custom Email Input -->
<div id="customEmailInput" style="display: {{ ($isEdit && $club->email) ? 'block' : 'none' }};">
<input type="email"
class="form-control"
id="email"
name="email"
value="{{ $club->email ?? old('email') }}"
placeholder="[email protected]">
<small class="text-muted">A dedicated email address for club communications</small>
</div>
</div>
<!-- Club Phone -->
<div class="mb-4">
<label class="form-label">Club Phone Number</label>
<!-- Toggle: Use Owner Phone or Custom -->
<div class="form-check mb-3">
<input class="form-check-input"
type="radio"
name="phone_option"
id="phone_option_owner"
value="owner"
{{ (!$isEdit || !$club->phone) ? 'checked' : '' }}>
<label class="form-check-label" for="phone_option_owner">
Use Club Owner's Phone
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input"
type="radio"
name="phone_option"
id="phone_option_custom"
value="custom"
{{ ($isEdit && $club->phone) ? 'checked' : '' }}>
<label class="form-check-label" for="phone_option_custom">
Use Custom Phone Number
</label>
</div>
<!-- Owner Phone Display (Read-only) -->
<div id="ownerPhoneDisplay" class="border rounded p-3 mb-3" style="background-color: hsl(var(--muted) / 0.2); display: {{ (!$isEdit || !$club->phone) ? 'block' : 'none' }};">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-phone text-muted"></i>
<span id="ownerPhoneText" class="text-muted">
@if($isEdit && $club->owner && $club->owner->mobile)
{{ $club->owner->mobile_formatted }}
@else
Select a club owner first
@endif
</span>
</div>
</div>
<!-- Custom Phone Input -->
<div id="customPhoneInput" style="display: {{ ($isEdit && $club->phone) ? 'block' : 'none' }};">
<x-country-code-dropdown
name="phone_code"
id="phone_code"
:value="$isEdit && $club->phone ? ($club->phone['code'] ?? '+973') : old('phone_code', '+973')"
:required="false"
:error="null">
<input type="text"
class="form-control"
name="phone_number"
id="phone_number"
value="{{ $isEdit && $club->phone ? ($club->phone['number'] ?? '') : old('phone_number') }}"
placeholder="12345678">
</x-country-code-dropdown>
<small class="text-muted">A dedicated phone number for club inquiries</small>
</div>
</div>
<!-- Additional Contact Info (Optional) -->
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle me-2"></i>
<strong>Tip:</strong> You can add more contact methods (WhatsApp, social media, website) in the <strong>Identity & Branding</strong> tab under Social Media Links.
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Email option toggle
const emailOptionOwner = document.getElementById('email_option_owner');
const emailOptionCustom = document.getElementById('email_option_custom');
const ownerEmailDisplay = document.getElementById('ownerEmailDisplay');
const customEmailInput = document.getElementById('customEmailInput');
const emailInput = document.getElementById('email');
if (emailOptionOwner && emailOptionCustom) {
emailOptionOwner.addEventListener('change', function() {
if (this.checked) {
ownerEmailDisplay.style.display = 'block';
customEmailInput.style.display = 'none';
if (emailInput) emailInput.value = '';
}
});
emailOptionCustom.addEventListener('change', function() {
if (this.checked) {
ownerEmailDisplay.style.display = 'none';
customEmailInput.style.display = 'block';
}
});
}
// Phone option toggle
const phoneOptionOwner = document.getElementById('phone_option_owner');
const phoneOptionCustom = document.getElementById('phone_option_custom');
const ownerPhoneDisplay = document.getElementById('ownerPhoneDisplay');
const customPhoneInput = document.getElementById('customPhoneInput');
const phoneNumberInput = document.getElementById('phone_number');
if (phoneOptionOwner && phoneOptionCustom) {
phoneOptionOwner.addEventListener('change', function() {
if (this.checked) {
ownerPhoneDisplay.style.display = 'block';
customPhoneInput.style.display = 'none';
if (phoneNumberInput) phoneNumberInput.value = '';
}
});
phoneOptionCustom.addEventListener('change', function() {
if (this.checked) {
ownerPhoneDisplay.style.display = 'none';
customPhoneInput.style.display = 'block';
}
});
}
// Update owner email/phone display when owner is selected
document.addEventListener('ownerSelected', function(e) {
const owner = e.detail;
// Update email display
const ownerEmailText = document.getElementById('ownerEmailText');
if (ownerEmailText && owner.email) {
ownerEmailText.textContent = owner.email;
}
// Update phone display
const ownerPhoneText = document.getElementById('ownerPhoneText');
if (ownerPhoneText && owner.mobile) {
ownerPhoneText.textContent = owner.mobile;
}
});
// Listen for owner selection from user picker
const ownerInput = document.getElementById('owner_user_id');
if (ownerInput) {
// Create a MutationObserver to watch for value changes
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
// Owner changed, update displays if using owner's contact info
updateOwnerContactInfo();
}
});
});
observer.observe(ownerInput, { attributes: true });
ownerInput.addEventListener('change', updateOwnerContactInfo);
}
});
function updateOwnerContactInfo() {
// This function would ideally fetch the owner's details
// For now, it's handled by the user picker modal
// which updates the display directly
}
</script>
@endpush
@@ -0,0 +1,346 @@
@props(['club' => null, 'mode' => 'create'])
@php
$isEdit = $mode === 'edit' && $club;
@endphp
<div class="container-fluid px-0">
<h5 class="fw-bold mb-3">Finance & Settings</h5>
<p class="text-muted mb-4">Configure bank accounts and club status</p>
<!-- Bank Accounts Section -->
<div class="mb-5">
<h6 class="fw-semibold mb-3">
<i class="bi bi-bank me-2"></i>Bank Accounts
</h6>
<p class="text-muted small mb-3">Add one or more bank accounts for receiving payments</p>
<div id="bankAccountsContainer">
@if($isEdit && $club->bankAccounts && $club->bankAccounts->count() > 0)
@foreach($club->bankAccounts as $index => $account)
<div class="bank-account-block border rounded p-4 mb-3" data-index="{{ $index }}" style="background-color: hsl(var(--muted) / 0.1);">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-semibold mb-0">Bank Account #{{ $index + 1 }}</h6>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeBankAccount(this)">
<i class="bi bi-trash"></i> Remove
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Bank Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control"
name="bank_accounts[{{ $index }}][bank_name]"
value="{{ $account->bank_name }}"
placeholder="e.g., National Bank of Bahrain"
required>
</div>
<div class="col-md-6">
<label class="form-label">Account Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control"
name="bank_accounts[{{ $index }}][account_name]"
value="{{ $account->account_name }}"
placeholder="e.g., Club Name Ltd."
required>
</div>
<div class="col-md-6">
<label class="form-label">Account Number</label>
<input type="text"
class="form-control"
name="bank_accounts[{{ $index }}][account_number]"
value="{{ $account->account_number }}"
placeholder="e.g., 1234567890">
</div>
<div class="col-md-6">
<label class="form-label">IBAN</label>
<input type="text"
class="form-control"
name="bank_accounts[{{ $index }}][iban]"
value="{{ $account->iban }}"
placeholder="e.g., BH67BMAG00001299123456"
pattern="[A-Z]{2}[0-9]{2}[A-Z0-9]+">
</div>
<div class="col-md-6">
<label class="form-label">SWIFT/BIC Code</label>
<input type="text"
class="form-control"
name="bank_accounts[{{ $index }}][swift_code]"
value="{{ $account->swift_code }}"
placeholder="e.g., BMAGBHBM"
pattern="[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?">
</div>
<div class="col-md-6">
<label class="form-label">BenefitPay Account</label>
<input type="text"
class="form-control"
name="bank_accounts[{{ $index }}][benefitpay_account]"
value="{{ $account->benefitpay_account ?? '' }}"
placeholder="Optional">
<small class="text-muted">Local payment system account number</small>
</div>
</div>
</div>
@endforeach
@endif
</div>
<button type="button" class="btn btn-outline-primary" onclick="addBankAccount()">
<i class="bi bi-plus-circle me-2"></i>Add Bank Account
</button>
</div>
<!-- Club Status & Visibility -->
<div class="mb-4">
<h6 class="fw-semibold mb-3">
<i class="bi bi-gear me-2"></i>Club Status & Visibility
</h6>
<div class="row g-3">
<!-- Club Status -->
<div class="col-md-6">
<label for="club_status" class="form-label">Club Status</label>
<select class="form-select" id="club_status" name="club_status">
<option value="active" {{ ($club->status ?? 'active') === 'active' ? 'selected' : '' }}>
Active
</option>
<option value="inactive" {{ ($club->status ?? '') === 'inactive' ? 'selected' : '' }}>
Inactive
</option>
<option value="pending" {{ ($club->status ?? '') === 'pending' ? 'selected' : '' }}>
Pending
</option>
</select>
<small class="text-muted">Current operational status of the club</small>
</div>
<!-- Public Profile -->
<div class="col-md-6">
<label class="form-label">Public Profile</label>
<div class="form-check form-switch" style="padding-top: 0.5rem;">
<input class="form-check-input"
type="checkbox"
role="switch"
id="public_profile_enabled"
name="public_profile_enabled"
value="1"
{{ ($club->public_profile_enabled ?? true) ? 'checked' : '' }}>
<label class="form-check-label" for="public_profile_enabled">
Enable public profile page
</label>
</div>
<small class="text-muted">Allow public access to club URL and QR code</small>
</div>
</div>
</div>
<!-- Enrollment Fee (Optional) -->
<div class="mb-4">
<label for="enrollment_fee" class="form-label">Enrollment Fee</label>
<div class="input-group">
<span class="input-group-text">{{ $club->currency ?? 'BHD' }}</span>
<input type="number"
class="form-control"
id="enrollment_fee"
name="enrollment_fee"
value="{{ $club->enrollment_fee ?? old('enrollment_fee', '0.00') }}"
min="0"
step="0.01"
placeholder="0.00">
</div>
<small class="text-muted">One-time fee for new members (optional)</small>
</div>
<!-- Summary Info Box -->
<div class="alert alert-light border" role="alert">
<h6 class="alert-heading fw-semibold">
<i class="bi bi-info-circle me-2"></i>Summary
</h6>
<ul class="mb-0 small">
<li><strong>Bank Accounts:</strong> <span id="bankAccountCount">{{ $isEdit && $club->bankAccounts ? $club->bankAccounts->count() : 0 }}</span> configured</li>
<li><strong>Status:</strong> Club will be set as <span id="statusSummary" class="fw-semibold">Active</span></li>
<li><strong>Public Access:</strong> <span id="publicAccessSummary" class="fw-semibold">Enabled</span></li>
</ul>
</div>
<!-- Metadata Info (Read-only) -->
@if($isEdit)
<div class="border-top pt-4 mt-4">
<h6 class="text-muted small text-uppercase mb-3">Metadata</h6>
<div class="row g-3 small text-muted">
<div class="col-md-6">
<i class="bi bi-calendar-plus me-2"></i>
<strong>Created:</strong> {{ $club->created_at->format('M d, Y') }}
</div>
<div class="col-md-6">
<i class="bi bi-calendar-check me-2"></i>
<strong>Last Updated:</strong> {{ $club->updated_at->format('M d, Y') }}
</div>
@if($club->owner)
<div class="col-md-12">
<i class="bi bi-person me-2"></i>
<strong>Owner:</strong> {{ $club->owner->full_name }}
</div>
@endif
</div>
</div>
@endif
</div>
@push('scripts')
<script>
let bankAccountIndex = {{ $isEdit && $club->bankAccounts ? $club->bankAccounts->count() : 0 }};
document.addEventListener('DOMContentLoaded', function() {
updateSummary();
// Update summary when status changes
const statusSelect = document.getElementById('club_status');
if (statusSelect) {
statusSelect.addEventListener('change', updateSummary);
}
// Update summary when public profile toggle changes
const publicProfileToggle = document.getElementById('public_profile_enabled');
if (publicProfileToggle) {
publicProfileToggle.addEventListener('change', updateSummary);
}
});
// Add bank account block
function addBankAccount() {
const container = document.getElementById('bankAccountsContainer');
if (!container) return;
const block = document.createElement('div');
block.className = 'bank-account-block border rounded p-4 mb-3';
block.style.backgroundColor = 'hsl(var(--muted) / 0.1)';
block.dataset.index = bankAccountIndex;
block.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-semibold mb-0">Bank Account #${bankAccountIndex + 1}</h6>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeBankAccount(this)">
<i class="bi bi-trash"></i> Remove
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Bank Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control"
name="bank_accounts[${bankAccountIndex}][bank_name]"
placeholder="e.g., National Bank of Bahrain"
required>
</div>
<div class="col-md-6">
<label class="form-label">Account Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control"
name="bank_accounts[${bankAccountIndex}][account_name]"
placeholder="e.g., Club Name Ltd."
required>
</div>
<div class="col-md-6">
<label class="form-label">Account Number</label>
<input type="text"
class="form-control"
name="bank_accounts[${bankAccountIndex}][account_number]"
placeholder="e.g., 1234567890">
</div>
<div class="col-md-6">
<label class="form-label">IBAN</label>
<input type="text"
class="form-control"
name="bank_accounts[${bankAccountIndex}][iban]"
placeholder="e.g., BH67BMAG00001299123456"
pattern="[A-Z]{2}[0-9]{2}[A-Z0-9]+">
</div>
<div class="col-md-6">
<label class="form-label">SWIFT/BIC Code</label>
<input type="text"
class="form-control"
name="bank_accounts[${bankAccountIndex}][swift_code]"
placeholder="e.g., BMAGBHBM"
pattern="[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?">
</div>
<div class="col-md-6">
<label class="form-label">BenefitPay Account</label>
<input type="text"
class="form-control"
name="bank_accounts[${bankAccountIndex}][benefitpay_account]"
placeholder="Optional">
<small class="text-muted">Local payment system account number</small>
</div>
</div>
`;
container.appendChild(block);
bankAccountIndex++;
updateSummary();
// Scroll to the new account
block.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Remove bank account block
function removeBankAccount(button) {
const block = button.closest('.bank-account-block');
if (block) {
if (confirm('Are you sure you want to remove this bank account?')) {
block.remove();
updateSummary();
renumberBankAccounts();
}
}
}
// Renumber bank accounts after removal
function renumberBankAccounts() {
const blocks = document.querySelectorAll('.bank-account-block');
blocks.forEach((block, index) => {
const heading = block.querySelector('h6');
if (heading) {
heading.textContent = `Bank Account #${index + 1}`;
}
});
}
// Update summary
function updateSummary() {
// Update bank account count
const bankAccountCount = document.querySelectorAll('.bank-account-block').length;
const countElement = document.getElementById('bankAccountCount');
if (countElement) {
countElement.textContent = bankAccountCount;
}
// Update status summary
const statusSelect = document.getElementById('club_status');
const statusSummary = document.getElementById('statusSummary');
if (statusSelect && statusSummary) {
const statusText = statusSelect.options[statusSelect.selectedIndex].text;
statusSummary.textContent = statusText;
// Update color based on status
statusSummary.className = 'fw-semibold';
if (statusSelect.value === 'active') {
statusSummary.classList.add('text-success');
} else if (statusSelect.value === 'inactive') {
statusSummary.classList.add('text-danger');
} else {
statusSummary.classList.add('text-warning');
}
}
// Update public access summary
const publicToggle = document.getElementById('public_profile_enabled');
const publicSummary = document.getElementById('publicAccessSummary');
if (publicToggle && publicSummary) {
publicSummary.textContent = publicToggle.checked ? 'Enabled' : 'Disabled';
publicSummary.className = publicToggle.checked ? 'fw-semibold text-success' : 'fw-semibold text-muted';
}
}
</script>
@endpush
@@ -0,0 +1,743 @@
@props(['club' => null, 'mode' => 'create'])
@php
$isEdit = $mode === 'edit' && $club;
@endphp
<div class="container-fluid px-0">
<h5 class="fw-bold mb-3">Identity & Branding</h5>
<p class="text-muted mb-4">Define your club's public identity, URL, and visual branding</p>
<!-- Club Slug -->
<div class="mb-4">
<label for="slug" class="form-label">
Club Slug <span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-white">
<i class="bi bi-link-45deg"></i>
</span>
<input type="text"
class="form-control"
id="slug"
name="slug"
value="{{ $club->slug ?? old('slug') }}"
required
pattern="[a-z0-9-]+"
data-error-message="Slug is required and must be URL-friendly"
placeholder="e.g., bh-taekwondo">
</div>
<small class="text-muted">URL-friendly identifier (lowercase letters, numbers, and hyphens only)</small>
<div class="invalid-feedback">Please enter a valid slug.</div>
</div>
<!-- Club URL Preview -->
<div class="mb-4">
<label class="form-label">Club Public URL</label>
<div class="border rounded p-3" style="background-color: hsl(var(--muted) / 0.2);">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-globe text-primary"></i>
<code id="clubUrlPreview" class="text-primary mb-0">{{ url('/club/') }}/{{ $club->slug ?? 'your-club-slug' }}</code>
<button type="button" class="btn btn-sm btn-outline-primary ms-auto" onclick="copyClubUrl()">
<i class="bi bi-clipboard"></i>
</button>
</div>
</div>
<small class="text-muted">This is the public URL where members can view your club</small>
</div>
<!-- QR Code -->
<div class="mb-4">
<label class="form-label">Club QR Code</label>
<div class="border rounded p-4 text-center" style="background-color: hsl(var(--muted) / 0.1);">
<div id="qrCodeContainer" class="d-inline-block mb-3"></div>
<div>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="downloadQRCode()">
<i class="bi bi-download me-2"></i>Download QR Code
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="printQRCode()">
<i class="bi bi-printer me-2"></i>Print
</button>
</div>
</div>
<small class="text-muted">Share this QR code for easy access to your club's page</small>
</div>
<!-- Logo and Cover Images -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label d-block">Club Logo <span class="text-danger">*</span></label>
<div class="text-center">
<!-- Logo Preview -->
<div class="cropper-preview-container mb-2" id="logoPreviewContainer">
@if($isEdit && $club->logo)
<img src="{{ asset('storage/' . $club->logo) }}"
id="logoPreview"
class="cropper-preview-image"
style="width: 150px; height: 150px; border-radius: 8px; border: 2px solid #dee2e6;">
@else
<div id="logoPreview"
class="cropper-preview-placeholder"
style="width: 150px; height: 150px; border-radius: 8px; border: 2px dashed #dee2e6; display: flex; align-items: center; justify-content: center; background-color: #f0f0f0; color: #6c757d;">
<i class="bi bi-image" style="font-size: 2rem;"></i>
</div>
@endif
</div>
<input type="hidden" name="logo" id="logoInput" value="{{ $isEdit && $club->logo ? $club->logo : '' }}">
<input type="hidden" name="logo_folder" value="clubs/logos">
<input type="hidden" name="logo_filename" value="logo_{{ time() }}">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="openLogoCropper()">
<i class="bi bi-camera me-2"></i>Upload Logo
</button>
<small class="text-muted d-block mt-2">Square image recommended (400x400px)</small>
<small class="text-muted d-block">Used as main logo and favicon</small>
</div>
</div>
<div class="col-md-6">
<label class="form-label d-block">Cover Image</label>
<div class="text-center">
<!-- Cover Preview -->
<div class="cropper-preview-container mb-2" id="coverPreviewContainer">
@if($isEdit && $club->cover_image)
<img src="{{ asset('storage/' . $club->cover_image) }}"
id="coverPreview"
class="cropper-preview-image"
style="width: 250px; height: 83px; border-radius: 8px; border: 2px solid #dee2e6;">
@else
<div id="coverPreview"
class="cropper-preview-placeholder"
style="width: 250px; height: 83px; border-radius: 8px; border: 2px dashed #dee2e6; display: flex; align-items: center; justify-content: center; background-color: #f0f0f0; color: #6c757d;">
<i class="bi bi-image" style="font-size: 2rem;"></i>
</div>
@endif
</div>
<input type="hidden" name="cover_image" id="coverInput" value="{{ $isEdit && $club->cover_image ? $club->cover_image : '' }}">
<input type="hidden" name="cover_image_folder" value="clubs/covers">
<input type="hidden" name="cover_image_filename" value="cover_{{ time() }}">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="openCoverCropper()">
<i class="bi bi-camera me-2"></i>Upload Cover
</button>
<small class="text-muted d-block mt-2">Wide banner image (1200x400px)</small>
<small class="text-muted d-block">Used for club profile header</small>
</div>
</div>
</div>
<!-- PART 2: Internal Cropper Overlays (NOT separate modals) -->
<!-- Logo Cropper Overlay -->
<div id="logoCropperOverlay" class="cropper-overlay" style="display: none;">
<div class="cropper-panel">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">Crop Logo</h5>
<button type="button" class="btn-close" onclick="closeLogoCropper()"></button>
</div>
<input type="file" id="logoFileInput" class="form-control form-control-sm mb-3" accept="image/*">
<div id="logoBox" class="takeone-canvas" style="height: 400px; background: #111; border-radius: 8px;"></div>
<div class="row mt-3">
<div class="col-6">
<label class="form-label small">Zoom</label>
<input type="range" class="form-range" id="logoZoom" min="0" max="100" step="1" value="0">
</div>
<div class="col-6">
<label class="form-label small">Rotation</label>
<input type="range" class="form-range" id="logoRotation" min="-180" max="180" step="1" value="0">
</div>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-secondary flex-fill" onclick="closeLogoCropper()">Cancel</button>
<button type="button" class="btn btn-primary flex-fill" onclick="saveLogoCrop()">Save & Apply</button>
</div>
</div>
</div>
<!-- Cover Cropper Overlay -->
<div id="coverCropperOverlay" class="cropper-overlay" style="display: none;">
<div class="cropper-panel">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">Crop Cover Image</h5>
<button type="button" class="btn-close" onclick="closeCoverCropper()"></button>
</div>
<input type="file" id="coverFileInput" class="form-control form-control-sm mb-3" accept="image/*">
<div id="coverBox" class="takeone-canvas" style="height: 400px; background: #111; border-radius: 8px;"></div>
<div class="row mt-3">
<div class="col-6">
<label class="form-label small">Zoom</label>
<input type="range" class="form-range" id="coverZoom" min="0" max="100" step="1" value="0">
</div>
<div class="col-6">
<label class="form-label small">Rotation</label>
<input type="range" class="form-range" id="coverRotation" min="-180" max="180" step="1" value="0">
</div>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-secondary flex-fill" onclick="closeCoverCropper()">Cancel</button>
<button type="button" class="btn btn-primary flex-fill" onclick="saveCoverCrop()">Save & Apply</button>
</div>
</div>
</div>
<!-- Social Media Links -->
<div class="mb-4">
<label class="form-label">Social Media Links</label>
<p class="text-muted small mb-3">Add links to your club's social media profiles</p>
<div id="socialLinksContainer">
@if($isEdit && $club->socialLinks && $club->socialLinks->count() > 0)
@foreach($club->socialLinks as $index => $link)
<div class="social-link-row mb-3" data-index="{{ $index }}">
<div class="row g-2">
<div class="col-md-4">
<select class="form-select" name="social_links[{{ $index }}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook" {{ $link->platform === 'facebook' ? 'selected' : '' }}>
<i class="bi bi-facebook"></i> Facebook
</option>
<option value="instagram" {{ $link->platform === 'instagram' ? 'selected' : '' }}>
Instagram
</option>
<option value="twitter" {{ $link->platform === 'twitter' ? 'selected' : '' }}>
X (Twitter)
</option>
<option value="tiktok" {{ $link->platform === 'tiktok' ? 'selected' : '' }}>
TikTok
</option>
<option value="youtube" {{ $link->platform === 'youtube' ? 'selected' : '' }}>
YouTube
</option>
<option value="whatsapp" {{ $link->platform === 'whatsapp' ? 'selected' : '' }}>
WhatsApp
</option>
<option value="website" {{ $link->platform === 'website' ? 'selected' : '' }}>
Website
</option>
</select>
</div>
<div class="col-md-7">
<input type="url"
class="form-control"
name="social_links[{{ $index }}][url]"
value="{{ $link->url }}"
placeholder="https://..."
required>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-outline-danger w-100" onclick="removeSocialLink(this)">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
@endforeach
@endif
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addSocialLink()">
<i class="bi bi-plus-circle me-2"></i>Add Social Link
</button>
</div>
</div>
@push('styles')
<style>
/* PART 2: Cropper Overlay Styles */
.cropper-overlay {
position: fixed; /* PART 1 FIX: Fixed positioning to cover entire modal */
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.85);
z-index: 1065; /* PART 1 FIX: Higher than modal content (1060) */
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
overflow-y: auto;
}
.cropper-panel {
background: white;
border-radius: 1rem;
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
}
.takeone-canvas {
position: relative;
border: 1px solid #222;
}
.cropper-preview-container {
position: relative;
display: inline-block;
}
.cropper-preview-placeholder {
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border: 2px dashed #dee2e6;
color: #6c757d;
}
.cropper-preview-image {
object-fit: cover;
border: 2px solid #dee2e6;
}
</style>
@endpush
@push('scripts')
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/cropme.min.css">
<script src="https://unpkg.com/[email protected]/dist/cropme.min.js"></script>
<script>
let socialLinkIndex = {{ $isEdit && $club->socialLinks ? $club->socialLinks->count() : 0 }};
let qrCode = null;
// PART 2: Cropper instances
let logoCropper = null;
let coverCropper = null;
const zoomMin = 0.01;
const zoomMax = 3;
document.addEventListener('DOMContentLoaded', function() {
// Update URL preview when slug changes
const slugInput = document.getElementById('slug');
const urlPreview = document.getElementById('clubUrlPreview');
if (slugInput && urlPreview) {
slugInput.addEventListener('input', function() {
const baseUrl = '{{ url("/club/") }}';
const slug = this.value || 'your-club-slug';
urlPreview.textContent = `${baseUrl}/${slug}`;
// Regenerate QR code
generateQRCode(`${baseUrl}/${slug}`);
});
// Mark slug as manually edited when user types
slugInput.addEventListener('keydown', function() {
this.dataset.manuallyEdited = 'true';
});
// Initial QR code generation
const initialUrl = urlPreview.textContent;
generateQRCode(initialUrl);
}
});
// Generate QR Code
function generateQRCode(url) {
const container = document.getElementById('qrCodeContainer');
if (!container) return;
// Clear existing QR code
container.innerHTML = '';
// Generate new QR code
qrCode = new QRCode(container, {
text: url,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
}
// Copy club URL to clipboard
function copyClubUrl() {
const urlPreview = document.getElementById('clubUrlPreview');
if (!urlPreview) return;
const url = urlPreview.textContent;
navigator.clipboard.writeText(url).then(() => {
// Show success feedback
if (typeof Toast !== 'undefined') {
Toast.success('Copied!', 'Club URL copied to clipboard');
} else {
alert('URL copied to clipboard!');
}
}).catch(err => {
console.error('Failed to copy:', err);
});
}
// Download QR Code
function downloadQRCode() {
const container = document.getElementById('qrCodeContainer');
if (!container) return;
const canvas = container.querySelector('canvas');
if (!canvas) return;
const url = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'club-qr-code.png';
link.href = url;
link.click();
}
// Print QR Code
function printQRCode() {
const container = document.getElementById('qrCodeContainer');
if (!container) return;
const canvas = container.querySelector('canvas');
if (!canvas) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head>
<title>Club QR Code</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
}
.container {
text-align: center;
}
img {
max-width: 400px;
height: auto;
}
h2 {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<img src="${canvas.toDataURL('image/png')}" alt="Club QR Code">
<h2>${document.getElementById('club_name')?.value || 'Club QR Code'}</h2>
<p>${document.getElementById('clubUrlPreview')?.textContent || ''}</p>
</div>
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
}
// Add social link row
function addSocialLink() {
const container = document.getElementById('socialLinksContainer');
if (!container) return;
const row = document.createElement('div');
row.className = 'social-link-row mb-3';
row.dataset.index = socialLinkIndex;
row.innerHTML = `
<div class="row g-2">
<div class="col-md-4">
<select class="form-select" name="social_links[${socialLinkIndex}][platform]" required>
<option value="">Select Platform</option>
<option value="facebook">Facebook</option>
<option value="instagram">Instagram</option>
<option value="twitter">X (Twitter)</option>
<option value="tiktok">TikTok</option>
<option value="youtube">YouTube</option>
<option value="whatsapp">WhatsApp</option>
<option value="website">Website</option>
</select>
</div>
<div class="col-md-7">
<input type="url"
class="form-control"
name="social_links[${socialLinkIndex}][url]"
placeholder="https://..."
required>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-outline-danger w-100" onclick="removeSocialLink(this)">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
container.appendChild(row);
socialLinkIndex++;
}
// Remove social link row
function removeSocialLink(button) {
const row = button.closest('.social-link-row');
if (row) {
row.remove();
}
}
// ============================================
// PART 2: Cropper Overlay Functions
// ============================================
// Helper function to apply transform
function applyTransform(instance) {
if (!instance.properties.image) return;
const p = instance.properties;
const t = `translate3d(${p.x}px, ${p.y}px, 0) scale(${p.scale}) rotate(${p.deg}deg)`;
p.image.style.transform = t;
}
// Initialize cropper with specific aspect ratio
function initCropper(elementId, width, height, shape, aspectRatio) {
const el = document.getElementById(elementId);
if (!el) return null;
const cropper = new Cropme(el, {
container: { width: '100%', height: 400 },
viewport: {
width: width,
height: height,
type: shape,
border: { enable: true, width: 2, color: '#fff' }
},
transformOrigin: 'viewport',
zoom: { min: zoomMin, max: zoomMax, enable: true, mouseWheel: true, slider: false },
rotation: { enable: true, slider: false }
});
return cropper;
}
// ===== LOGO CROPPER =====
function openLogoCropper() {
const overlay = document.getElementById('logoCropperOverlay');
overlay.style.display = 'flex';
// Prevent main modal body from scrolling
const modalBody = document.querySelector('#clubModal .modal-body');
if (modalBody) modalBody.style.overflow = 'hidden';
}
function closeLogoCropper() {
const overlay = document.getElementById('logoCropperOverlay');
overlay.style.display = 'none';
// Restore main modal body scrolling
const modalBody = document.querySelector('#clubModal .modal-body');
if (modalBody) modalBody.style.overflow = 'auto';
// Destroy cropper
if (logoCropper) {
logoCropper.destroy();
logoCropper = null;
}
// Reset file input
document.getElementById('logoFileInput').value = '';
}
function saveLogoCrop() {
if (!logoCropper) return;
// Logo: Crop to 400x400 square
logoCropper.crop({
type: 'base64',
width: 400,
height: 400
}).then(base64 => {
// Store in hidden input
document.getElementById('logoInput').value = base64;
// Update preview
const preview = document.getElementById('logoPreview');
if (preview && preview.tagName === 'IMG') {
preview.src = base64;
} else {
const container = document.getElementById('logoPreviewContainer');
if (container) {
container.innerHTML = `
<img src="${base64}"
id="logoPreview"
class="cropper-preview-image"
style="width: 150px; height: 150px; border-radius: 8px; border: 2px solid #dee2e6;">
`;
}
}
// Close overlay
closeLogoCropper();
});
}
// Logo file input handler
document.addEventListener('DOMContentLoaded', function() {
const logoFileInput = document.getElementById('logoFileInput');
if (logoFileInput) {
logoFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(event) {
if (logoCropper) logoCropper.destroy();
// Logo: Square aspect ratio (400x400)
logoCropper = initCropper('logoBox', 400, 400, 'square', 1);
if (logoCropper) {
logoCropper.bind({ url: event.target.result }).then(() => {
document.getElementById('logoZoom').value = 0;
document.getElementById('logoRotation').value = 0;
});
}
};
reader.readAsDataURL(this.files[0]);
}
});
}
// Logo zoom handler
const logoZoom = document.getElementById('logoZoom');
if (logoZoom) {
logoZoom.addEventListener('input', function() {
if (!logoCropper || !logoCropper.properties.image) return;
const p = parseFloat(this.value);
const scale = zoomMin + (zoomMax - zoomMin) * (p / 100);
logoCropper.properties.scale = Math.min(Math.max(scale, zoomMin), zoomMax);
applyTransform(logoCropper);
});
}
// Logo rotation handler
const logoRotation = document.getElementById('logoRotation');
if (logoRotation) {
logoRotation.addEventListener('input', function() {
if (logoCropper) {
logoCropper.rotate(parseInt(this.value, 10));
}
});
}
});
// ===== COVER CROPPER =====
function openCoverCropper() {
const overlay = document.getElementById('coverCropperOverlay');
overlay.style.display = 'flex';
// Prevent main modal body from scrolling
const modalBody = document.querySelector('#clubModal .modal-body');
if (modalBody) modalBody.style.overflow = 'hidden';
}
function closeCoverCropper() {
const overlay = document.getElementById('coverCropperOverlay');
overlay.style.display = 'none';
// Restore main modal body scrolling
const modalBody = document.querySelector('#clubModal .modal-body');
if (modalBody) modalBody.style.overflow = 'auto';
// Destroy cropper
if (coverCropper) {
coverCropper.destroy();
coverCropper = null;
}
// Reset file input
document.getElementById('coverFileInput').value = '';
}
function saveCoverCrop() {
if (!coverCropper) return;
// Cover: Crop to 1200x400 wide banner (3:1 aspect ratio)
coverCropper.crop({
type: 'base64',
width: 1200,
height: 400
}).then(base64 => {
// Store in hidden input for COVER (not logo!)
document.getElementById('coverInput').value = base64;
// Update COVER preview (not logo!)
const preview = document.getElementById('coverPreview');
if (preview && preview.tagName === 'IMG') {
preview.src = base64;
} else {
const container = document.getElementById('coverPreviewContainer');
if (container) {
container.innerHTML = `
<img src="${base64}"
id="coverPreview"
class="cropper-preview-image"
style="width: 250px; height: 83px; border-radius: 8px; border: 2px solid #dee2e6;">
`;
}
}
// Close overlay
closeCoverCropper();
});
}
// Cover file input handler
document.addEventListener('DOMContentLoaded', function() {
const coverFileInput = document.getElementById('coverFileInput');
if (coverFileInput) {
coverFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(event) {
if (coverCropper) coverCropper.destroy();
// Cover: Wide banner aspect ratio (600x200 = 3:1) - fits in container
// Will be scaled up to 1200x400 on save
coverCropper = initCropper('coverBox', 600, 200, 'square', 3);
if (coverCropper) {
coverCropper.bind({ url: event.target.result }).then(() => {
document.getElementById('coverZoom').value = 0;
document.getElementById('coverRotation').value = 0;
});
}
};
reader.readAsDataURL(this.files[0]);
}
});
}
// Cover zoom handler
const coverZoom = document.getElementById('coverZoom');
if (coverZoom) {
coverZoom.addEventListener('input', function() {
if (!coverCropper || !coverCropper.properties.image) return;
const p = parseFloat(this.value);
const scale = zoomMin + (zoomMax - zoomMin) * (p / 100);
coverCropper.properties.scale = Math.min(Math.max(scale, zoomMin), zoomMax);
applyTransform(coverCropper);
});
}
// Cover rotation handler
const coverRotation = document.getElementById('coverRotation');
if (coverRotation) {
coverRotation.addEventListener('input', function() {
if (coverCropper) {
coverCropper.rotate(parseInt(this.value, 10));
}
});
}
});
</script>
@endpush
@@ -0,0 +1,541 @@
@props(['club' => null, 'mode' => 'create'])
@php
$isEdit = $mode === 'edit' && $club;
@endphp
<div class="container-fluid px-0">
<h5 class="fw-bold mb-3">Location</h5>
<p class="text-muted mb-4">Set your club's geographic location and regional settings</p>
<!-- Country, Timezone, Currency Row -->
<div class="row mb-4">
<div class="col-md-4">
<x-nationality-dropdown
name="country"
id="country"
label="Country"
:value="$club->country ?? old('country', 'Bahrain')"
:required="true"
:error="null" />
</div>
<div class="col-md-4">
<x-timezone-dropdown-bootstrap
name="timezone"
id="timezone"
:value="$club->timezone ?? old('timezone', 'Asia/Bahrain')"
:required="false"
:error="null" />
</div>
<div class="col-md-4">
<x-currency-dropdown-bootstrap
name="currency"
id="currency"
:value="$club->currency ?? old('currency', 'BHD')"
:required="false"
:error="null" />
</div>
</div>
<!-- Address -->
<div class="mb-4">
<label for="address" class="form-label">Street Address</label>
<textarea class="form-control"
id="address"
name="address"
rows="2"
placeholder="Enter the full street address of your club">{{ $club->address ?? old('address') }}</textarea>
<small class="text-muted">Full address including building number, street name, area, etc.</small>
</div>
<!-- Map -->
<div class="mb-4">
<label class="form-label">Location on Map</label>
<div id="modalClubMap" style="height: 400px; border-radius: 0.5rem; border: 1px solid hsl(var(--border));"></div>
<small class="text-muted">Drag the marker to set the exact location of your club</small>
</div>
<!-- GPS Coordinates -->
<div class="row mb-4">
<div class="col-md-6">
<label for="gps_lat" class="form-label">
<i class="bi bi-geo-alt me-1"></i>Latitude
</label>
<input type="number"
class="form-control"
id="gps_lat"
name="gps_lat"
value="{{ $club->gps_lat ?? old('gps_lat') }}"
step="0.0000001"
min="-90"
max="90"
placeholder="e.g., 26.0667">
<small class="text-muted">Decimal degrees (-90 to 90)</small>
</div>
<div class="col-md-6">
<label for="gps_long" class="form-label">
<i class="bi bi-geo-alt me-1"></i>Longitude
</label>
<input type="number"
class="form-control"
id="gps_long"
name="gps_long"
value="{{ $club->gps_long ?? old('gps_long') }}"
step="0.0000001"
min="-180"
max="180"
placeholder="e.g., 50.5577">
<small class="text-muted">Decimal degrees (-180 to 180)</small>
</div>
</div>
<!-- Google Maps Link -->
<div class="mb-4">
<label for="google_maps_link" class="form-label">
<i class="bi bi-google me-1"></i>Google Maps Link (Optional)
</label>
<div class="input-group">
<span class="input-group-text bg-white">
<i class="bi bi-link-45deg"></i>
</span>
<input type="url"
class="form-control"
id="google_maps_link"
name="google_maps_link"
placeholder="Paste Google Maps share link here..."
pattern="https?://.*google\.com/maps.*|https?://goo\.gl/maps/.*">
</div>
<small class="text-muted">Paste a Google Maps share URL to auto-fill coordinates</small>
</div>
<!-- Quick Location Actions -->
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="getCurrentLocation()">
<i class="bi bi-crosshair me-2"></i>Use My Current Location
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="centerOnCountry()">
<i class="bi bi-globe me-2"></i>Center on Selected Country
</button>
</div>
</div>
@push('scripts')
<script>
let clubMap = null;
let clubMarker = null;
let countriesData = null;
let mapInitialized = false;
document.addEventListener('DOMContentLoaded', function() {
// Load countries data
fetch('/data/countries.json')
.then(response => response.json())
.then(countries => {
countriesData = countries;
// PART 1A: Device-based preselection on modal open (create mode only)
const clubForm = document.getElementById('clubForm');
if (clubForm && clubForm.dataset.mode === 'create') {
detectAndPreselectCountries(countries);
}
setupLocationHandlers();
})
.catch(error => console.error('Error loading countries:', error));
// Initialize map when location tab is shown OR when modal opens with location tab active
const locationTab = document.getElementById('location-tab');
const clubModal = document.getElementById('clubModal');
// Function to initialize map if not already done
function tryInitializeMap() {
if (!mapInitialized && locationTab && locationTab.classList.contains('active')) {
setTimeout(() => {
initializeMap();
mapInitialized = true;
}, 100);
} else if (clubMap) {
// ISSUE 4 FIX: Fix gray tiles by invalidating size
setTimeout(() => {
clubMap.invalidateSize();
}, 100);
}
}
// Listen for tab shown event
if (locationTab) {
locationTab.addEventListener('shown.bs.tab', tryInitializeMap);
}
// Listen for modal shown event (for edit mode when location tab is already active)
if (clubModal) {
clubModal.addEventListener('shown.bs.modal', function() {
// Reset map initialization flag when modal opens
mapInitialized = false;
// Try to initialize map after modal is fully shown
setTimeout(tryInitializeMap, 150);
});
// Clean up map when modal closes
clubModal.addEventListener('hidden.bs.modal', function() {
if (clubMap) {
clubMap.remove();
clubMap = null;
clubMarker = null;
mapInitialized = false;
}
});
}
});
// PART 1A: Detect device location and preselect country/timezone/currency
function detectAndPreselectCountries(countries) {
if (!navigator.geolocation) {
// Fallback to default (Bahrain)
preselectCountryData('Bahrain', countries);
return;
}
navigator.geolocation.getCurrentPosition(
function(position) {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
// Use reverse geocoding to get country
fetch(`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`)
.then(response => response.json())
.then(data => {
const countryName = data.countryName || 'Bahrain';
preselectCountryData(countryName, countries);
})
.catch(() => {
// Fallback to default
preselectCountryData('Bahrain', countries);
});
},
function() {
// Geolocation denied or failed, use default
preselectCountryData('Bahrain', countries);
}
);
}
// Preselect country, timezone, and currency based on detected/default country
function preselectCountryData(countryName, countries) {
const country = countries.find(c => c.name.toLowerCase() === countryName.toLowerCase());
if (!country) return;
// Preselect country
const countryInput = document.getElementById('country');
if (countryInput && !countryInput.value) {
countryInput.value = country.iso3 || country.name;
countryInput.dispatchEvent(new Event('change'));
}
// Preselect timezone using Bootstrap dropdown
const timezoneInput = document.getElementById('timezone');
if (timezoneInput && country.timezone && !timezoneInput.value && typeof setTimezoneValue === 'function') {
setTimezoneValue('timezone', country.timezone, countries);
}
// Preselect currency using Bootstrap dropdown
const currencyInput = document.getElementById('currency');
if (currencyInput && country.currency && !currencyInput.value && typeof setCurrencyValue === 'function') {
setCurrencyValue('currency', country.currency, countries);
}
// Set map center to country
if (country.latitude && country.longitude) {
const lat = parseFloat(country.latitude);
const lng = parseFloat(country.longitude);
if (!isNaN(lat) && !isNaN(lng)) {
const latInput = document.getElementById('gps_lat');
const lngInput = document.getElementById('gps_long');
if (latInput && !latInput.value) latInput.value = lat.toFixed(7);
if (lngInput && !lngInput.value) lngInput.value = lng.toFixed(7);
}
}
}
// Initialize Leaflet map
function initializeMap() {
const mapElement = document.getElementById('modalClubMap');
if (!mapElement) return;
const lat = parseFloat(document.getElementById('gps_lat')?.value) || 26.0667;
const lng = parseFloat(document.getElementById('gps_long')?.value) || 50.5577;
// ISSUE 4 FIX: Initialize map without attribution control
clubMap = L.map('clubMap', {
attributionControl: false // Disable attribution
}).setView([lat, lng], 13);
// ISSUE 4 FIX: Add tile layer without attribution text
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '', // Empty attribution
maxZoom: 19
}).addTo(clubMap);
// Add draggable marker
clubMarker = L.marker([lat, lng], {
draggable: true
}).addTo(clubMap);
// Update coordinates when marker is dragged
clubMarker.on('dragend', function(e) {
const position = e.target.getLatLng();
updateCoordinates(position.lat, position.lng);
});
// Update marker when coordinates are manually entered
const latInput = document.getElementById('gps_lat');
const lngInput = document.getElementById('gps_long');
if (latInput && lngInput) {
latInput.addEventListener('change', function() {
updateMarkerPosition();
});
lngInput.addEventListener('change', function() {
updateMarkerPosition();
});
}
// ISSUE 4 FIX: Fix initial rendering after a short delay
setTimeout(() => {
if (clubMap) {
clubMap.invalidateSize();
}
}, 100);
}
// Update coordinates in inputs
function updateCoordinates(lat, lng) {
const latInput = document.getElementById('gps_lat');
const lngInput = document.getElementById('gps_long');
if (latInput) latInput.value = lat.toFixed(7);
if (lngInput) lngInput.value = lng.toFixed(7);
}
// Update marker position from inputs
function updateMarkerPosition() {
const lat = parseFloat(document.getElementById('gps_lat')?.value);
const lng = parseFloat(document.getElementById('gps_long')?.value);
if (!isNaN(lat) && !isNaN(lng) && clubMarker && clubMap) {
const newPos = L.latLng(lat, lng);
clubMarker.setLatLng(newPos);
clubMap.setView(newPos, clubMap.getZoom());
}
}
// Setup location-related event handlers
function setupLocationHandlers() {
// Watch for country changes
const countryInput = document.getElementById('country');
if (countryInput) {
// Use MutationObserver to detect value changes
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
handleCountryChange(countryInput.value);
}
});
});
observer.observe(countryInput, { attributes: true });
// Also listen for direct changes
countryInput.addEventListener('change', function() {
handleCountryChange(this.value);
});
// Check periodically for value changes (fallback)
let lastCountryValue = countryInput.value;
setInterval(function() {
if (countryInput.value !== lastCountryValue) {
lastCountryValue = countryInput.value;
handleCountryChange(countryInput.value);
}
}, 500);
}
// Parse Google Maps link
const googleMapsInput = document.getElementById('google_maps_link');
if (googleMapsInput) {
googleMapsInput.addEventListener('change', function() {
parseGoogleMapsLink(this.value);
});
}
}
// PART 1B: Handle country change - update timezone, currency, and map
function handleCountryChange(countryName) {
if (!countriesData || !countryName) return;
const country = countriesData.find(c =>
c.name.toLowerCase() === countryName.toLowerCase() ||
c.iso3 === countryName
);
if (!country) return;
// PART 1B: Always update timezone to match country using Bootstrap dropdown
if (country.timezone && typeof setTimezoneValue === 'function') {
setTimezoneValue('timezone', country.timezone, countriesData);
}
// PART 1B: Always update currency to match country using Bootstrap dropdown
if (country.currency && typeof setCurrencyValue === 'function') {
setCurrencyValue('currency', country.currency, countriesData);
}
// Center map on country
if (country.latitude && country.longitude) {
const lat = parseFloat(country.latitude);
const lng = parseFloat(country.longitude);
if (!isNaN(lat) && !isNaN(lng)) {
// Update coordinates
const latInput = document.getElementById('gps_lat');
const lngInput = document.getElementById('gps_long');
// Only update if empty or user wants to recenter
const shouldUpdate = !latInput?.value || !lngInput?.value;
if (shouldUpdate) {
updateCoordinates(lat, lng);
}
// Always recenter map view on country
if (clubMarker && clubMap) {
if (shouldUpdate) {
clubMarker.setLatLng([lat, lng]);
}
clubMap.setView([lat, lng], 10);
}
}
}
}
// Get current location using browser geolocation
function getCurrentLocation() {
if (!navigator.geolocation) {
alert('Geolocation is not supported by your browser');
return;
}
// Show loading state
const btn = event.target.closest('button');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Getting location...';
navigator.geolocation.getCurrentPosition(
function(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
updateCoordinates(lat, lng);
if (clubMarker && clubMap) {
clubMarker.setLatLng([lat, lng]);
clubMap.setView([lat, lng], 15);
}
btn.disabled = false;
btn.innerHTML = originalHtml;
if (typeof Toast !== 'undefined') {
Toast.success('Success', 'Location updated successfully');
}
},
function(error) {
console.error('Geolocation error:', error);
alert('Unable to get your location. Please check your browser permissions.');
btn.disabled = false;
btn.innerHTML = originalHtml;
}
);
}
// Center map on selected country
function centerOnCountry() {
const countryInput = document.getElementById('country');
if (!countryInput || !countriesData) return;
const countryName = countryInput.value;
const country = countriesData.find(c =>
c.name.toLowerCase() === countryName.toLowerCase() ||
c.iso3 === countryName
);
if (country && country.latitude && country.longitude) {
const lat = parseFloat(country.latitude);
const lng = parseFloat(country.longitude);
if (!isNaN(lat) && !isNaN(lng) && clubMap) {
clubMap.setView([lat, lng], 10);
if (typeof Toast !== 'undefined') {
Toast.info('Info', `Centered on ${country.name}`);
}
}
}
}
// Parse Google Maps link to extract coordinates
function parseGoogleMapsLink(url) {
if (!url) return;
try {
// Try to extract coordinates from various Google Maps URL formats
let lat, lng;
// Format: @lat,lng
const atMatch = url.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
if (atMatch) {
lat = parseFloat(atMatch[1]);
lng = parseFloat(atMatch[2]);
}
// Format: !3d and !4d
if (!lat || !lng) {
const dMatch = url.match(/!3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/);
if (dMatch) {
lat = parseFloat(dMatch[1]);
lng = parseFloat(dMatch[2]);
}
}
// Format: ll=lat,lng
if (!lat || !lng) {
const llMatch = url.match(/ll=(-?\d+\.\d+),(-?\d+\.\d+)/);
if (llMatch) {
lat = parseFloat(llMatch[1]);
lng = parseFloat(llMatch[2]);
}
}
if (lat && lng && !isNaN(lat) && !isNaN(lng)) {
updateCoordinates(lat, lng);
if (clubMarker && clubMap) {
clubMarker.setLatLng([lat, lng]);
clubMap.setView([lat, lng], 15);
}
if (typeof Toast !== 'undefined') {
Toast.success('Success', 'Coordinates extracted from Google Maps link');
}
} else {
if (typeof Toast !== 'undefined') {
Toast.warning('Warning', 'Could not extract coordinates from the link');
}
}
} catch (error) {
console.error('Error parsing Google Maps link:', error);
if (typeof Toast !== 'undefined') {
Toast.error('Error', 'Invalid Google Maps link');
}
}
}
</script>
@endpush
@@ -0,0 +1,204 @@
@props(['name' => 'currency', 'id' => 'currency', 'value' => '', 'required' => false, 'error' => null, 'label' => 'Currency'])
<div class="mb-3">
<label for="{{ $id }}" class="form-label">{{ $label }}</label>
<div class="dropdown w-100" onclick="event.stopPropagation()">
<button class="form-select dropdown-toggle d-flex align-items-center justify-content-between @error($name) is-invalid @enderror"
type="button"
id="{{ $id }}Dropdown"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
style="text-align: left; background-color: rgba(255,255,255,0.8);">
<span class="d-flex align-items-center">
<span id="{{ $id }}SelectedFlag"></span>
<span class="currency-label" id="{{ $id }}SelectedCurrency">Select Currency</span>
</span>
</button>
<div class="dropdown-menu p-2 w-100" aria-labelledby="{{ $id }}Dropdown" onclick="event.stopPropagation()">
<input type="text"
class="form-control form-control-sm mb-2"
placeholder="Search currency..."
id="{{ $id }}Search"
onmousedown="event.stopPropagation()"
onfocus="event.stopPropagation()"
oninput="event.stopPropagation()"
onkeydown="event.stopPropagation()"
onkeyup="event.stopPropagation()">
<div class="currency-list" id="{{ $id }}List" data-component-id="{{ $id }}" style="max-height: 300px; overflow-y: auto;">
<!-- Currencies will be populated by JavaScript -->
</div>
</div>
<input type="hidden" id="{{ $id }}" name="{{ $name }}" value="{{ $value }}" {{ $required ? 'required' : '' }}>
</div>
@if($error)
<span class="invalid-feedback d-block" role="alert">
<strong>{{ $error }}</strong>
</span>
@endif
</div>
@once
@push('styles')
<style>
.currency-list {
max-height: 300px;
overflow-y: auto;
}
.currency-list .dropdown-item {
cursor: pointer;
}
.currency-list .dropdown-item:hover {
background-color: #f8f9fa;
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load countries from JSON file
fetch('/data/countries.json')
.then(response => response.json())
.then(countries => {
// Initialize all currency dropdowns
document.querySelectorAll('.currency-list').forEach(function(listElement) {
const componentId = listElement.getAttribute('data-component-id');
initializeCurrencyDropdown(componentId, countries);
});
})
.catch(error => console.error('Error loading countries:', error));
function initializeCurrencyDropdown(componentId, countries) {
const currencyList = document.getElementById(componentId + 'List');
if (!currencyList) return;
// Clear existing items
currencyList.innerHTML = '';
// Get unique currencies with their associated countries
const currencyMap = {};
countries.forEach(country => {
if (country.currency && !currencyMap[country.currency]) {
currencyMap[country.currency] = {
currency: country.currency,
flag: country.iso2,
countryName: country.name
};
}
});
// Populate currency dropdown
Object.values(currencyMap).forEach(currData => {
const button = document.createElement('button');
button.className = 'dropdown-item d-flex align-items-center';
button.type = 'button';
button.setAttribute('data-currency-code', currData.currency);
button.setAttribute('data-country-name', currData.countryName);
button.setAttribute('data-flag', currData.flag);
button.setAttribute('data-search', `${currData.countryName.toLowerCase()} ${currData.currency.toLowerCase()}`);
// Convert flag code to emoji
const flagEmoji = currData.flag
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
// Format: "🇧🇭 Bahrain – BHD"
button.innerHTML = `
<span class="me-2">${flagEmoji}</span>
<span>${currData.countryName} – ${currData.currency}</span>
`;
button.addEventListener('click', function() {
selectCurrency(componentId, currData.currency, flagEmoji, currData.countryName);
});
currencyList.appendChild(button);
});
// Search functionality
const searchInput = document.getElementById(componentId + 'Search');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const items = currencyList.querySelectorAll('.dropdown-item');
items.forEach(item => {
const searchText = item.getAttribute('data-search') || '';
if (searchText.includes(searchTerm)) {
item.classList.remove('d-none');
} else {
item.classList.add('d-none');
}
});
});
}
// Set initial value if provided
const hiddenInput = document.getElementById(componentId);
if (hiddenInput && hiddenInput.value) {
const initialCurrency = Object.values(currencyMap).find(curr => curr.currency === hiddenInput.value);
if (initialCurrency) {
const flagEmoji = initialCurrency.flag
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
selectCurrency(componentId, initialCurrency.currency, flagEmoji, initialCurrency.countryName);
}
}
}
function selectCurrency(componentId, currency, flag, countryName) {
const flagElement = document.getElementById(componentId + 'SelectedFlag');
const currencyElement = document.getElementById(componentId + 'SelectedCurrency');
const hiddenInput = document.getElementById(componentId);
if (flagElement) flagElement.textContent = flag + ' ';
if (currencyElement) currencyElement.textContent = `${countryName} – ${currency}`;
if (hiddenInput) {
hiddenInput.value = currency;
// Trigger change event for other handlers
hiddenInput.dispatchEvent(new Event('change'));
}
// Close the dropdown after selection
const dropdownButton = document.getElementById(componentId + 'Dropdown');
if (dropdownButton) {
const dropdown = bootstrap.Dropdown.getInstance(dropdownButton);
if (dropdown) dropdown.hide();
}
}
// Expose function globally for external use
window.setCurrencyValue = function(componentId, currency, countries) {
const currencyMap = {};
countries.forEach(country => {
if (country.currency && !currencyMap[country.currency]) {
currencyMap[country.currency] = {
currency: country.currency,
flag: country.iso2,
countryName: country.name
};
}
});
const currData = currencyMap[currency];
if (currData) {
const flagEmoji = currData.flag
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
selectCurrency(componentId, currency, flagEmoji, currData.countryName);
}
};
});
</script>
@endpush
@endonce
@@ -42,12 +42,14 @@
}
});
// Populate dropdown
// Populate dropdown with enhanced format: Flag + Country Name – Currency Code
Object.values(uniqueCurrencies).forEach(currencyData => {
const option = document.createElement('option');
option.value = currencyData.currency;
option.textContent = `${currencyData.currency} - ${currencyData.name} ${currencyData.currency_symbol}`;
// Format: "Bahrain – BHD"
option.textContent = `${currencyData.name} – ${currencyData.currency}`;
option.setAttribute('data-flag', currencyData.flag);
option.setAttribute('data-country', currencyData.name);
selectElement.appendChild(option);
});
@@ -56,7 +58,7 @@
selectElement.value = initialValue;
}
// Initialize Select2 for searchable dropdown
// Initialize Select2 for searchable dropdown with flags
if (typeof $ !== 'undefined' && $.fn.select2) {
$(selectElement).select2({
templateResult: function(state) {
@@ -65,7 +67,9 @@
}
const option = $(state.element);
const flagCode = option.data('flag');
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
// Show flag emoji + text
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
return $(`<span>${flagEmoji} ${state.text}</span>`);
},
templateSelection: function(state) {
if (!state.id) {
@@ -73,9 +77,25 @@
}
const option = $(state.element);
const flagCode = option.data('flag');
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
// Show flag emoji + text
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
return $(`<span>${flagEmoji} ${state.text}</span>`);
},
width: '100%'
width: '100%',
// Enable search by country name or currency code
matcher: function(params, data) {
if ($.trim(params.term) === '') {
return data;
}
const term = params.term.toLowerCase();
const text = data.text.toLowerCase();
const country = $(data.element).data('country');
if (text.indexOf(term) > -1 || (country && country.toLowerCase().indexOf(term) > -1)) {
return data;
}
return null;
}
});
}
});
@@ -0,0 +1,201 @@
@props(['name' => 'timezone', 'id' => 'timezone', 'value' => '', 'required' => false, 'error' => null, 'label' => 'Timezone'])
<div class="mb-3">
<label for="{{ $id }}" class="form-label">{{ $label }}</label>
<div class="dropdown w-100" onclick="event.stopPropagation()">
<button class="form-select dropdown-toggle d-flex align-items-center justify-content-between @error($name) is-invalid @enderror"
type="button"
id="{{ $id }}Dropdown"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
style="text-align: left; background-color: rgba(255,255,255,0.8);">
<span class="d-flex align-items-center">
<span id="{{ $id }}SelectedFlag"></span>
<span class="timezone-label" id="{{ $id }}SelectedTimezone">Select Timezone</span>
</span>
</button>
<div class="dropdown-menu p-2 w-100" aria-labelledby="{{ $id }}Dropdown" onclick="event.stopPropagation()">
<input type="text"
class="form-control form-control-sm mb-2"
placeholder="Search timezone..."
id="{{ $id }}Search"
onmousedown="event.stopPropagation()"
onfocus="event.stopPropagation()"
oninput="event.stopPropagation()"
onkeydown="event.stopPropagation()"
onkeyup="event.stopPropagation()">
<div class="timezone-list" id="{{ $id }}List" data-component-id="{{ $id }}" style="max-height: 300px; overflow-y: auto;">
<!-- Timezones will be populated by JavaScript -->
</div>
</div>
<input type="hidden" id="{{ $id }}" name="{{ $name }}" value="{{ $value }}" {{ $required ? 'required' : '' }}>
</div>
@if($error)
<span class="invalid-feedback d-block" role="alert">
<strong>{{ $error }}</strong>
</span>
@endif
</div>
@once
@push('styles')
<style>
.timezone-list {
max-height: 300px;
overflow-y: auto;
}
.timezone-list .dropdown-item {
cursor: pointer;
}
.timezone-list .dropdown-item:hover {
background-color: #f8f9fa;
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load countries from JSON file
fetch('/data/countries.json')
.then(response => response.json())
.then(countries => {
// Initialize all timezone dropdowns
document.querySelectorAll('.timezone-list').forEach(function(listElement) {
const componentId = listElement.getAttribute('data-component-id');
initializeTimezoneDropdown(componentId, countries);
});
})
.catch(error => console.error('Error loading countries:', error));
function initializeTimezoneDropdown(componentId, countries) {
const timezoneList = document.getElementById(componentId + 'List');
if (!timezoneList) return;
// Clear existing items
timezoneList.innerHTML = '';
// Get unique timezones with their associated countries
const timezoneMap = {};
countries.forEach(country => {
if (country.timezone && !timezoneMap[country.timezone]) {
timezoneMap[country.timezone] = {
timezone: country.timezone,
flag: country.iso2,
countryName: country.name
};
}
});
// Populate timezone dropdown
Object.values(timezoneMap).forEach(tzData => {
const button = document.createElement('button');
button.className = 'dropdown-item d-flex align-items-center';
button.type = 'button';
button.setAttribute('data-timezone', tzData.timezone);
button.setAttribute('data-flag', tzData.flag);
button.setAttribute('data-search', `${tzData.countryName.toLowerCase()} ${tzData.timezone.toLowerCase()}`);
// Convert flag code to emoji
const flagEmoji = tzData.flag
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
button.innerHTML = `
<span class="me-2">${flagEmoji}</span>
<span>${tzData.timezone}</span>
`;
button.addEventListener('click', function() {
selectTimezone(componentId, tzData.timezone, flagEmoji);
});
timezoneList.appendChild(button);
});
// Search functionality
const searchInput = document.getElementById(componentId + 'Search');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const items = timezoneList.querySelectorAll('.dropdown-item');
items.forEach(item => {
const searchText = item.getAttribute('data-search') || '';
if (searchText.includes(searchTerm)) {
item.classList.remove('d-none');
} else {
item.classList.add('d-none');
}
});
});
}
// Set initial value if provided
const hiddenInput = document.getElementById(componentId);
if (hiddenInput && hiddenInput.value) {
const initialTimezone = Object.values(timezoneMap).find(tz => tz.timezone === hiddenInput.value);
if (initialTimezone) {
const flagEmoji = initialTimezone.flag
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
selectTimezone(componentId, initialTimezone.timezone, flagEmoji);
}
}
}
function selectTimezone(componentId, timezone, flag) {
const flagElement = document.getElementById(componentId + 'SelectedFlag');
const timezoneElement = document.getElementById(componentId + 'SelectedTimezone');
const hiddenInput = document.getElementById(componentId);
if (flagElement) flagElement.textContent = flag + ' ';
if (timezoneElement) timezoneElement.textContent = timezone;
if (hiddenInput) {
hiddenInput.value = timezone;
// Trigger change event for other handlers
hiddenInput.dispatchEvent(new Event('change'));
}
// Close the dropdown after selection
const dropdownButton = document.getElementById(componentId + 'Dropdown');
if (dropdownButton) {
const dropdown = bootstrap.Dropdown.getInstance(dropdownButton);
if (dropdown) dropdown.hide();
}
}
// Expose function globally for external use
window.setTimezoneValue = function(componentId, timezone, countries) {
const timezoneMap = {};
countries.forEach(country => {
if (country.timezone && !timezoneMap[country.timezone]) {
timezoneMap[country.timezone] = {
timezone: country.timezone,
flag: country.iso2
};
}
});
const tzData = timezoneMap[timezone];
if (tzData) {
const flagEmoji = tzData.flag
.toUpperCase()
.split('')
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
selectTimezone(componentId, timezone, flagEmoji);
}
};
});
</script>
@endpush
@endonce
@@ -41,12 +41,13 @@
}
});
// Populate dropdown
// Populate dropdown with country name and timezone
Object.values(uniqueTimezones).forEach(timezoneData => {
const option = document.createElement('option');
option.value = timezoneData.timezone;
option.textContent = `${timezoneData.name} (${timezoneData.timezone})`;
option.textContent = `${timezoneData.timezone}`;
option.setAttribute('data-flag', timezoneData.flag);
option.setAttribute('data-country', timezoneData.name);
selectElement.appendChild(option);
});
@@ -55,7 +56,7 @@
selectElement.value = initialValue;
}
// Initialize Select2 for searchable dropdown
// Initialize Select2 for searchable dropdown with flag emojis
if (typeof $ !== 'undefined' && $.fn.select2) {
$(selectElement).select2({
templateResult: function(state) {
@@ -64,7 +65,9 @@
}
const option = $(state.element);
const flagCode = option.data('flag');
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
// Convert ISO2 code to flag emoji
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
return $(`<span>${flagEmoji} ${state.text}</span>`);
},
templateSelection: function(state) {
if (!state.id) {
@@ -72,7 +75,9 @@
}
const option = $(state.element);
const flagCode = option.data('flag');
return $(`<span><span class="fi fi-${flagCode} me-2"></span>${state.text}</span>`);
// Convert ISO2 code to flag emoji
const flagEmoji = flagCode ? String.fromCodePoint(...[...flagCode.toUpperCase()].map(c => 127397 + c.charCodeAt())) : '';
return $(`<span>${flagEmoji} ${state.text}</span>`);
},
width: '100%'
});
@@ -0,0 +1,276 @@
<!-- User Picker Modal -->
<div class="modal fade" id="userPickerModal" tabindex="-1" aria-labelledby="userPickerModalLabel" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" style="border-radius: 1rem; border: none;">
<!-- Modal Header -->
<div class="modal-header border-0">
<div>
<h5 class="modal-title fw-bold" id="userPickerModalLabel">Select Club Owner</h5>
<p class="text-muted small mb-0">Search and select a user to be the club owner</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Modal Body -->
<div class="modal-body">
<!-- Search Input -->
<div class="mb-4">
<div class="input-group">
<span class="input-group-text bg-white">
<i class="bi bi-search"></i>
</span>
<input type="text"
class="form-control"
id="userSearchInput"
placeholder="Search by name, email, or phone..."
autocomplete="off">
</div>
</div>
<!-- Loading State -->
<div id="userPickerLoading" class="text-center py-5" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="text-muted mt-2">Searching users...</p>
</div>
<!-- Users List -->
<div id="userPickerResults" style="max-height: 400px; overflow-y: auto;">
<!-- Results will be populated here -->
</div>
<!-- No Results -->
<div id="userPickerNoResults" class="text-center py-5" style="display: none;">
<i class="bi bi-person-x fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">No users found</p>
<small class="text-muted">Try a different search term</small>
</div>
</div>
</div>
</div>
</div>
@once
@push('scripts')
<script>
(function() {
const userPickerModal = document.getElementById('userPickerModal');
if (!userPickerModal) return;
const searchInput = document.getElementById('userSearchInput');
const resultsContainer = document.getElementById('userPickerResults');
const loadingDiv = document.getElementById('userPickerLoading');
const noResultsDiv = document.getElementById('userPickerNoResults');
let searchTimeout;
let allUsers = [];
// Prevent club modal from closing when user picker opens
userPickerModal.addEventListener('show.bs.modal', function() {
const clubModal = document.getElementById('clubModal');
if (clubModal) {
clubModal.style.display = 'block';
}
});
// Load all users when modal opens
userPickerModal.addEventListener('shown.bs.modal', function() {
searchInput.value = '';
searchInput.focus();
loadUsers();
});
// Ensure club modal stays visible when user picker closes
userPickerModal.addEventListener('hidden.bs.modal', function() {
const clubModal = document.getElementById('clubModal');
if (clubModal && clubModal.classList.contains('show')) {
// Keep club modal visible
document.body.classList.add('modal-open');
const backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
backdrop.style.display = 'block';
}
}
});
// Search with debounce
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
filterUsers(this.value);
}, 300);
});
// Load users from server
async function loadUsers() {
showLoading();
try {
const response = await fetch('/admin/api/users', {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
if (response.ok) {
allUsers = await response.json();
displayUsers(allUsers);
} else {
showError('Failed to load users');
}
} catch (error) {
console.error('Error loading users:', error);
showError('An error occurred while loading users');
}
}
// Filter users based on search term
function filterUsers(searchTerm) {
if (!searchTerm.trim()) {
displayUsers(allUsers);
return;
}
const term = searchTerm.toLowerCase();
const filtered = allUsers.filter(user => {
return (
user.full_name.toLowerCase().includes(term) ||
user.email.toLowerCase().includes(term) ||
(user.mobile && user.mobile.toLowerCase().includes(term))
);
});
displayUsers(filtered);
}
// Display users in the list
function displayUsers(users) {
hideLoading();
if (users.length === 0) {
resultsContainer.style.display = 'none';
noResultsDiv.style.display = 'block';
return;
}
resultsContainer.style.display = 'block';
noResultsDiv.style.display = 'none';
resultsContainer.innerHTML = users.map(user => `
<div class="user-card border rounded p-3 mb-2"
style="cursor: pointer; transition: all 0.2s;"
data-user-id="${user.id}"
data-user-name="${user.full_name}"
data-user-email="${user.email}"
data-user-mobile="${user.mobile || ''}"
data-user-picture="${user.profile_picture || ''}"
onmouseover="this.style.backgroundColor='hsl(var(--muted) / 0.5)'; this.style.transform='translateX(4px)';"
onmouseout="this.style.backgroundColor=''; this.style.transform='';">
<div class="d-flex align-items-center gap-3">
${user.profile_picture ? `
<img src="${user.profile_picture}"
alt="${user.full_name}"
class="rounded-circle"
style="width: 50px; height: 50px; object-fit: cover;">
` : `
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">
${user.full_name.charAt(0).toUpperCase()}
</div>
`}
<div class="flex-grow-1">
<div class="fw-semibold">${user.full_name}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>${user.email}
${user.mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile}</span>` : ''}
</div>
</div>
<div>
<i class="bi bi-check-circle text-primary" style="font-size: 1.5rem;"></i>
</div>
</div>
</div>
`).join('');
// Attach click handlers
resultsContainer.querySelectorAll('.user-card').forEach(card => {
card.addEventListener('click', function() {
selectUser({
id: this.dataset.userId,
name: this.dataset.userName,
email: this.dataset.userEmail,
mobile: this.dataset.userMobile,
picture: this.dataset.userPicture
});
});
});
}
// Select a user
function selectUser(user) {
// Update hidden input
const ownerInput = document.getElementById('owner_user_id');
if (ownerInput) {
ownerInput.value = user.id;
ownerInput.dispatchEvent(new Event('change'));
}
// Update display
const ownerDisplay = document.getElementById('ownerDisplay');
if (ownerDisplay) {
ownerDisplay.innerHTML = `
<div class="d-flex align-items-center gap-3">
${user.picture ? `
<img src="${user.picture}"
alt="${user.name}"
class="rounded-circle"
style="width: 50px; height: 50px; object-fit: cover;">
` : `
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center"
style="width: 50px; height: 50px; font-size: 1.25rem; font-weight: 600;">
${user.name.charAt(0).toUpperCase()}
</div>
`}
<div class="flex-grow-1">
<div class="fw-semibold">${user.name}</div>
<div class="small text-muted">
<i class="bi bi-envelope me-1"></i>${user.email}
${user.mobile ? `<span class="ms-2"><i class="bi bi-phone me-1"></i>${user.mobile}</span>` : ''}
</div>
</div>
</div>
`;
}
// Close modal
bootstrap.Modal.getInstance(userPickerModal).hide();
}
// Show loading state
function showLoading() {
loadingDiv.style.display = 'block';
resultsContainer.style.display = 'none';
noResultsDiv.style.display = 'none';
}
// Hide loading state
function hideLoading() {
loadingDiv.style.display = 'none';
}
// Show error
function showError(message) {
hideLoading();
resultsContainer.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>${message}
</div>
`;
resultsContainer.style.display = 'block';
}
})();
</script>
@endpush
@endonce