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

@@ -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