added club create and edit modal
This commit is contained in:
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
|
||||
Reference in new issue
Block a user