forked from UKSOURCE/cms.hailearning.edu.vn
1482 lines
71 KiB
Plaintext
1482 lines
71 KiB
Plaintext
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin="" />
|
|
|
|
<div class="container">
|
|
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-0" style="color: var(--primary-dark);"><%= title %></h1>
|
|
<p class="text-muted mb-0">Edit content displayed on Camp Location page</p>
|
|
</div>
|
|
<div>
|
|
<a href="<%= frontendUrl %>/destinations/" class="btn btn-outline-primary" target="_blank">
|
|
<i class="fas fa-external-link-alt me-2"></i>View Camp Location Page
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<form action="/admin/camp-location/update" method="POST" class="content-with-fixed-buttons" id="campLocationForm">
|
|
<!-- Hidden inputs for JSON data -->
|
|
<input type="hidden" name="metadata" id="metadataJson">
|
|
<input type="hidden" name="hero" id="heroJson">
|
|
<input type="hidden" name="camps" id="campsJson">
|
|
<input type="hidden" name="locations" id="locationsJson">
|
|
<input type="hidden" name="locationsSection" id="locationsSectionJson">
|
|
<input type="hidden" name="intro" id="introJson">
|
|
<input type="hidden" name="map" id="mapJson">
|
|
<input type="hidden" name="faq" id="faqJson">
|
|
<input type="hidden" name="faqSection" id="faqSectionJson">
|
|
<input type="hidden" name="welcomeQuote" id="welcomeQuoteJson">
|
|
<input type="hidden" name="securityConcept" id="securityConceptJson">
|
|
|
|
<!-- Navigation Tabs -->
|
|
<div class="card shadow-sm border-0 mb-4">
|
|
<div class="card-header bg-white border-bottom">
|
|
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
|
|
<i class="fas fa-home me-2"></i>Hero
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#welcomeQuote" role="tab">
|
|
<i class="fas fa-quote-left me-2"></i>Welcome Quote
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#intro" role="tab">
|
|
<i class="fas fa-info-circle me-2"></i>Intro
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#map" role="tab">
|
|
<i class="fas fa-map me-2"></i>Map & Camps
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#locations" role="tab">
|
|
<i class="fas fa-map-marker-alt me-2"></i>Locations
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#faq" role="tab">
|
|
<i class="fas fa-question-circle me-2"></i>FAQ
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#security" role="tab">
|
|
<i class="fas fa-shield-alt me-2"></i>Security
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
<!-- Hero Tab -->
|
|
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Title</label>
|
|
<input type="text" class="form-control" id="heroTitle" value="<%= data.hero?.title || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Background Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" id="heroBackgroundImage" value="<%= data.hero?.backgroundImage || '' %>" placeholder="yootheme/banner/image.jpg">
|
|
<button type="button" class="btn btn-outline-primary" onclick="document.getElementById('heroImageUpload').click()" data-image-type="banner">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<input type="file" id="heroImageUpload" accept="image/*" style="display: none;" onchange="handleHeroImageUpload(this)" data-image-type="banner">
|
|
<small class="form-text text-muted">Recommended size: 1920x1080px. Upload or enter image path.</small>
|
|
</div>
|
|
<div class="col-md-12" id="heroImagePreviewContainer">
|
|
<% if (data.hero?.backgroundImage) { %>
|
|
<img src="<%= data.hero.backgroundImage %>" id="heroImagePreview" class="img-thumbnail" style="max-height: 300px; width: 100%; object-fit: cover;" alt="Hero background preview">
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Welcome Quote Tab -->
|
|
<div class="tab-pane fade" id="welcomeQuote" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Title</label>
|
|
<input type="text" class="form-control" id="welcomeQuoteTitle" value="<%= data.welcomeQuote?.title || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Quote</label>
|
|
<textarea class="form-control" id="welcomeQuoteText" rows="4"><%= data.welcomeQuote?.quote || '' %></textarea>
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Author</label>
|
|
<input type="text" class="form-control" id="welcomeQuoteAuthor" value="<%= data.welcomeQuote?.author || '' %>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Intro Tab -->
|
|
<div class="tab-pane fade" id="intro" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Introduction Content</label>
|
|
<textarea class="form-control" id="introContent" rows="6"><%= data.intro?.content || '' %></textarea>
|
|
<small class="form-text text-muted">General introduction text displayed in IntroSection component</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Locations Tab -->
|
|
<div class="tab-pane fade" id="locations" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="mb-4">
|
|
<h6 class="fw-medium">Section Title</h6>
|
|
<input type="text" class="form-control" id="locationsSectionTitle" value="<%= data.locationsSection?.title || '' %>">
|
|
</div>
|
|
<div class="mb-4">
|
|
<h6 class="fw-medium">Read More Button Text</h6>
|
|
<input type="text" class="form-control" id="locationsSectionButtonText" value="<%= data.locationsSection?.readMoreButtonText || 'read more' %>" placeholder="read more">
|
|
<small class="form-text text-muted">Text displayed on location cards' read more button</small>
|
|
</div>
|
|
<hr>
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">Locations</h6>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="addLocation()">
|
|
<i class="fas fa-plus"></i> Add Location
|
|
</button>
|
|
</div>
|
|
<div id="locationsContainer">
|
|
<% if (data.locations && data.locations.length > 0) { %>
|
|
<% data.locations.forEach((location, index) => { %>
|
|
<div class="card mb-3 location-item border-start border-info border-3">
|
|
<div class="card-body">
|
|
<!-- Hidden ID field -->
|
|
<input type="hidden" name="locationId_<%= index %>" value="<%= location.id %>">
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Country</label>
|
|
<input type="text" class="form-control" name="locationCountry_<%= index %>" value="<%= location.country %>">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label">Title</label>
|
|
<input type="text" class="form-control" name="locationTitle_<%= index %>" value="<%= location.title %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Image Source</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" name="locationImageSrc_<%= index %>" id="locationImageSrc_<%= index %>" value="<%= location.imageSrc %>" placeholder="/upload/image.jpg">
|
|
<button type="button" class="btn btn-outline-primary" onclick="document.getElementById('locationImageUpload_<%= index %>').click()" data-image-type="locations">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<input type="file" id="locationImageUpload_<%= index %>" accept="image/*" style="display: none;" onchange="handleLocationImageUpload(this, <%= index %>)" data-image-type="locations">
|
|
<small class="form-text text-muted">Recommended size: 1200x800px. Upload or enter image path.</small>
|
|
</div>
|
|
<div class="col-md-12" id="locationImagePreviewContainer_<%= index %>">
|
|
<% if (location.imageSrc) { %>
|
|
<img src="<%= location.imageSrc %>" class="img-thumbnail" style="max-height: 200px; max-width: 400px; object-fit: cover;" alt="Location image preview">
|
|
<% } %>
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Image Alt Text</label>
|
|
<input type="text" class="form-control" name="locationImageAlt_<%= index %>" value="<%= location.imageAlt %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Image Position</label>
|
|
<select class="form-control" name="locationImagePosition_<%= index %>">
|
|
<option value="left" <%= location.imagePosition === 'left' ? 'selected' : '' %>>Left</option>
|
|
<option value="right" <%= location.imagePosition === 'right' ? 'selected' : '' %>>Right</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Card Size</label>
|
|
<select class="form-control" name="locationCardSize_<%= index %>">
|
|
<option value="default" <%= location.cardSize === 'default' ? 'selected' : '' %>>Default</option>
|
|
<option value="large" <%= location.cardSize === 'large' ? 'selected' : '' %>>Large</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Read More Link</label>
|
|
<input type="text" class="form-control" name="locationReadMoreLink_<%= index %>" value="<%= location.readMoreLink %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Scrollspy Class (optional)</label>
|
|
<input type="text" class="form-control" name="locationScrollspyClass_<%= index %>" value="<%= location.scrollspyClass || '' %>">
|
|
</div>
|
|
|
|
<!-- Program Options -->
|
|
<div class="col-md-12">
|
|
<hr class="my-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label fw-medium mb-0">Program Options</label>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addProgramOption(<%= index %>)">
|
|
<i class="fas fa-plus"></i> Add Program
|
|
</button>
|
|
</div>
|
|
<div class="program-options-container" data-location-index="<%= index %>">
|
|
<% if (location.programOptions && location.programOptions.length > 0) { %>
|
|
<% location.programOptions.forEach((program, pIndex) => { %>
|
|
<div class="card mb-2 program-option-item bg-light">
|
|
<div class="card-body p-2">
|
|
<div class="row g-2">
|
|
<div class="col-md-6">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Title" name="programTitle_<%= index %>_<%= pIndex %>" value="<%= program.title %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Link (href)" name="programHref_<%= index %>_<%= pIndex %>" value="<%= program.href %>">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Image Source" name="programImageSrc_<%= index %>_<%= pIndex %>" id="programImageSrc_<%= index %>_<%= pIndex %>" value="<%= program.imageSrc %>">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="document.getElementById('programImageUpload_<%= index %>_<%= pIndex %>').click()" data-image-type="programs">
|
|
<i class="fas fa-upload"></i>
|
|
</button>
|
|
</div>
|
|
<input type="file" id="programImageUpload_<%= index %>_<%= pIndex %>" accept="image/*" style="display: none;" onchange="handleProgramImageUpload(this, <%= index %>, <%= pIndex %>)" data-image-type="programs">
|
|
<small class="form-text text-muted">Recommended: 600x400px</small>
|
|
</div>
|
|
<div class="col-md-4" id="programImagePreviewContainer_<%= index %>_<%= pIndex %>">
|
|
<% if (program.imageSrc) { %>
|
|
<img src="<%= program.imageSrc %>" class="img-thumbnail mt-1" style="max-height: 80px; max-width: 120px; object-fit: cover;" alt="Program image preview">
|
|
<% } %>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Description" name="programDescription_<%= index %>_<%= pIndex %>" value="<%= program.description %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeProgramOption(this)">
|
|
<i class="fas fa-times"></i> Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeLocation(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Location
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FAQ Tab -->
|
|
<div class="tab-pane fade" id="faq" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="mb-4">
|
|
<h6 class="fw-medium">Section Title</h6>
|
|
<input type="text" class="form-control" id="faqSectionTitle" value="<%= data.faqSection?.title || '' %>">
|
|
</div>
|
|
<div class="mb-4">
|
|
<h6 class="fw-medium">FAQ Button Settings</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Button Text</label>
|
|
<input type="text" class="form-control" id="faqButtonText" value="<%= data.faqSection?.buttonText || 'More questions' %>" placeholder="More questions">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Button Icon</label>
|
|
<input type="text" class="form-control" id="faqButtonIcon" value="<%= data.faqSection?.buttonIcon || 'comments' %>" placeholder="comments">
|
|
<small class="form-text text-muted">UIkit icon name (e.g., comments, question, info)</small>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Button Link</label>
|
|
<input type="text" class="form-control" id="faqButtonLink" value="<%= data.faqSection?.buttonLink || '/info/faq' %>" placeholder="/info/faq">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr>
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">FAQ Items</h6>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="addFaq()">
|
|
<i class="fas fa-plus"></i> Add FAQ
|
|
</button>
|
|
</div>
|
|
<div id="faqContainer">
|
|
<% if (data.faq && data.faq.length > 0) { %>
|
|
<% data.faq.forEach((faqItem, index) => { %>
|
|
<div class="card mb-3 faq-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label">Question</label>
|
|
<input type="text" class="form-control" name="faqQuestion_<%= index %>" value="<%= faqItem.question %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Answer</label>
|
|
<textarea class="form-control" name="faqAnswer_<%= index %>" rows="3"><%= faqItem.answer %></textarea>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFaq(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove FAQ
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Security Concept Tab -->
|
|
<div class="tab-pane fade" id="security" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="mb-4">
|
|
<h6 class="fw-medium">Section Title</h6>
|
|
<input type="text" class="form-control" id="securityConceptTitle" value="<%= data.securityConcept?.title || '' %>">
|
|
</div>
|
|
<div class="mb-4">
|
|
<h6 class="fw-medium">Introduction</h6>
|
|
<textarea class="form-control" id="securityConceptIntroduction" rows="4"><%= data.securityConcept?.introduction || '' %></textarea>
|
|
<small class="form-text text-muted">Introduction text for parents about the security concept</small>
|
|
</div>
|
|
<hr>
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">Security Measures</h6>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="addSecurityItem()">
|
|
<i class="fas fa-plus"></i> Add Item
|
|
</button>
|
|
</div>
|
|
<div id="securityContainer">
|
|
<% if (data.securityConcept && data.securityConcept.items && data.securityConcept.items.length > 0) { %>
|
|
<% data.securityConcept.items.forEach((item, index) => { %>
|
|
<div class="card mb-3 security-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label">Title</label>
|
|
<input type="text" class="form-control" name="securityTitle_<%= index %>" value="<%= item.title %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Content</label>
|
|
<textarea class="form-control" name="securityContent_<%= index %>" rows="4"><%= item.content %></textarea>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeSecurityItem(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Item
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Tab -->
|
|
<div class="tab-pane fade" id="map" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<h6 class="fw-medium mb-3">Camp Locations Map</h6>
|
|
|
|
<!-- Manage Camps Section -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<h6 class="fw-medium mb-0">Manage Camp Markers</h6>
|
|
<small class="text-muted">Add camps with GPS coordinates to display on the map</small>
|
|
</div>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="addCamp()">
|
|
<i class="fas fa-plus me-1"></i>Add Camp
|
|
</button>
|
|
</div>
|
|
|
|
<div id="campsContainer">
|
|
<% if (data.camps && data.camps.length > 0) { %>
|
|
<% data.camps.forEach((camp, index) => { %>
|
|
<div class="card mb-3 camp-item border-start border-primary border-3">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<!-- Left Column: Image -->
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-medium">Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control form-control-sm" name="campImage_<%= index %>" value="<%= camp.image || '' %>" placeholder="URL or path">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="document.getElementById('campImageUpload_<%= index %>').click()" data-image-type="camps">
|
|
<i class="fas fa-upload"></i> Upload
|
|
</button>
|
|
</div>
|
|
<input type="file" id="campImageUpload_<%= index %>" accept="image/*" style="display: none;" onchange="handleCampImageUpload(this, <%= index %>)" data-image-type="camps">
|
|
<small class="form-text text-muted d-block mb-2">Recommended size: 800x600px</small>
|
|
<div id="campImagePreview_<%= index %>">
|
|
<% if (camp.image) { %>
|
|
<img src="<%= camp.image %>" class="img-thumbnail" style="width: 100%; max-height: 200px; object-fit: cover;" alt="Camp image preview">
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Fields -->
|
|
<div class="col-md-8">
|
|
<!-- Hidden ID field -->
|
|
<input type="hidden" name="campId_<%= index %>" value="<%= camp.id %>">
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label">Title</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" name="campTitle_<%= index %>" value="<%= camp.title || '' %>" placeholder="e.g., Hamburg" id="campTitle_<%= index %>">
|
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="geocodeCampLocation(<%= index %>)" title="Find coordinates">
|
|
<i class="fas fa-map-marker-alt"></i> Find
|
|
</button>
|
|
</div>
|
|
<small class="text-muted">Click "Find" to auto-fill coordinates</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Latitude</label>
|
|
<input type="number" step="0.0001" class="form-control" name="campLat_<%= index %>" value="<%= camp.lat || '' %>" placeholder="e.g., 53.5597" id="campLat_<%= index %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Longitude</label>
|
|
<input type="number" step="0.0001" class="form-control" name="campLng_<%= index %>" value="<%= camp.lng || '' %>" placeholder="e.g., 9.9601" id="campLng_<%= index %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<div id="geocodeStatus_<%= index %>" class="small"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeCamp(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Camp
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } else { %>
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>No camps added yet. Click "Add Camp" to create your first camp marker.
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<!-- Map Preview Section -->
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">Map Preview</h6>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="reloadCampMap()">
|
|
<i class="fas fa-sync-alt me-1"></i>Refresh Map
|
|
</button>
|
|
</div>
|
|
<% if (data.camps && data.camps.filter(c => c.lat && c.lng).length > 0) { %>
|
|
<div class="alert alert-success mb-3">
|
|
<i class="fas fa-check-circle me-2"></i>
|
|
Showing <strong><%= data.camps.filter(c => c.lat && c.lng).length %></strong> camp location(s) on the map
|
|
</div>
|
|
<% } else { %>
|
|
<div class="alert alert-warning mb-3">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
No camps with coordinates yet. Add latitude and longitude to camps above to see them on the map.
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
|
|
<!-- Map Preview -->
|
|
<div class="col-md-12">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body p-0">
|
|
<div id="mapPreview" style="height: 500px; width: 100%; border-radius: 4px; background: #f5f5f5; position: relative; overflow: hidden;">
|
|
<% if (data.camps && data.camps.length > 0) { %>
|
|
<%
|
|
const validCamps = data.camps.filter(c => c.lat && c.lng);
|
|
if (validCamps.length > 0) {
|
|
%>
|
|
<!-- Map will be rendered here by JavaScript -->
|
|
<% } else { %>
|
|
<div class="text-center p-4" style="height: 100%; display: flex; align-items: center; justify-content: center;">
|
|
<div>
|
|
<i class="fas fa-map-marked-alt fa-3x mb-3 text-muted"></i>
|
|
<p class="mb-0 text-muted">Add coordinates to camps above to see them on the map</p>
|
|
</div>
|
|
</div>
|
|
<% } %>
|
|
<% } else { %>
|
|
<div class="text-center p-4" style="height: 100%; display: flex; align-items: center; justify-content: center;">
|
|
<div>
|
|
<i class="fas fa-campground fa-3x mb-3 text-muted"></i>
|
|
<p class="mb-0 text-muted">No camps added yet. Click "Add Camp" button above to get started.</p>
|
|
</div>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
<% if (data.camps && data.camps.filter(c => c.lat && c.lng).length > 0) { %>
|
|
<div class="card-footer bg-light text-center">
|
|
<small class="text-muted">
|
|
<i class="fas fa-map-marker-alt me-1"></i>
|
|
Showing <%= data.camps.filter(c => c.lat && c.lng).length %> camp location(s)
|
|
</small>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Hidden fields for map settings - keep defaults -->
|
|
<input type="hidden" id="mapMarkerTitle" value="">
|
|
<input type="hidden" id="mapLocation" value="">
|
|
<input type="hidden" id="mapLat" value="0">
|
|
<input type="hidden" id="mapLng" value="0">
|
|
<input type="hidden" id="mapZoom" value="5">
|
|
<input type="hidden" id="tileLayerUrl" value="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png">
|
|
<input type="hidden" id="tileLayerAttribution" value="">
|
|
<input type="hidden" id="tileLayerMaxZoom" value="18">
|
|
<input type="hidden" id="tileLayerMinZoom" value="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fixed Bottom Buttons -->
|
|
<div class="fixed-bottom-buttons">
|
|
<button type="reset" class="btn btn-secondary">
|
|
<i class="fas fa-undo me-2"></i>Reset
|
|
</button>
|
|
<button type="submit" class="btn btn-primary" id="submitBtn">
|
|
<i class="fas fa-save me-2"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
<script>
|
|
let originalFormData = null;
|
|
let campMap = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
originalFormData = <%- JSON.stringify(data) %>;
|
|
updateAllJsonInputs(originalFormData);
|
|
initializeFormHandlers();
|
|
initializeCampMap();
|
|
|
|
// Re-initialize map when Map tab is shown
|
|
const mapTab = document.querySelector('a[href="#map"]');
|
|
if (mapTab) {
|
|
mapTab.addEventListener('shown.bs.tab', function() {
|
|
if (campMap) {
|
|
setTimeout(() => {
|
|
campMap.invalidateSize();
|
|
}, 100);
|
|
} else {
|
|
initializeCampMap();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Save active tab to localStorage when switching tabs
|
|
const tabLinks = document.querySelectorAll('a[data-bs-toggle="tab"]');
|
|
tabLinks.forEach(link => {
|
|
link.addEventListener('shown.bs.tab', function(e) {
|
|
localStorage.setItem('campLocationActiveTab', e.target.getAttribute('href'));
|
|
});
|
|
});
|
|
|
|
// Restore active tab after page reload (e.g., after save)
|
|
const savedTab = localStorage.getItem('campLocationActiveTab');
|
|
if (savedTab) {
|
|
const tabToActivate = document.querySelector(`a[href="${savedTab}"]`);
|
|
if (tabToActivate) {
|
|
const tab = new bootstrap.Tab(tabToActivate);
|
|
tab.show();
|
|
}
|
|
}
|
|
|
|
// Check if page was just updated (from URL parameter)
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.has('updated')) {
|
|
// Scroll to top to show success message
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
|
// Clean URL by removing the parameter
|
|
const cleanUrl = window.location.pathname;
|
|
window.history.replaceState({}, document.title, cleanUrl);
|
|
}
|
|
});
|
|
|
|
function initializeCampMap() {
|
|
// Wait for Leaflet to be fully loaded
|
|
setTimeout(() => {
|
|
try {
|
|
const mapContainer = document.getElementById('mapPreview');
|
|
if (!mapContainer) {
|
|
console.error('Map container not found');
|
|
return;
|
|
}
|
|
|
|
// Get current camps from form (live data)
|
|
const validCamps = [];
|
|
document.querySelectorAll('.camp-item').forEach((item, index) => {
|
|
const lat = parseFloat(item.querySelector(`[name="campLat_${index}"]`)?.value);
|
|
const lng = parseFloat(item.querySelector(`[name="campLng_${index}"]`)?.value);
|
|
const title = item.querySelector(`[name="campTitle_${index}"]`)?.value;
|
|
const image = item.querySelector(`[name="campImage_${index}"]`)?.value;
|
|
|
|
if (!isNaN(lat) && !isNaN(lng) && lat && lng) {
|
|
validCamps.push({
|
|
lat,
|
|
lng,
|
|
title: title || 'Camp',
|
|
image: image || ''
|
|
});
|
|
}
|
|
});
|
|
|
|
if (validCamps.length === 0) {
|
|
console.log('No camps with coordinates');
|
|
// Don't reinitialize if already showing placeholder
|
|
if (!mapContainer.querySelector('.fa-map-marked-alt')) {
|
|
mapContainer.innerHTML = '<div class="text-center p-4" style="height: 100%; display: flex; align-items: center; justify-content: center;"><div><i class="fas fa-map-marked-alt fa-3x mb-3 text-muted"></i><p class="mb-0 text-muted">Add coordinates to camps to see them on the map</p></div></div>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Destroy existing map if it exists
|
|
if (campMap) {
|
|
try {
|
|
campMap.remove();
|
|
campMap = null;
|
|
} catch (e) {
|
|
console.log('Error removing old map:', e);
|
|
}
|
|
}
|
|
|
|
// Clear any existing content and reset styles
|
|
mapContainer.innerHTML = '';
|
|
mapContainer.style.display = 'block';
|
|
mapContainer.style.height = '500px';
|
|
mapContainer.style.width = '100%';
|
|
|
|
// Initialize map
|
|
campMap = L.map('mapPreview', {
|
|
center: [51.5, 10],
|
|
zoom: 5,
|
|
scrollWheelZoom: true
|
|
});
|
|
|
|
// Add tile layer
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
maxZoom: 18,
|
|
minZoom: 3
|
|
}).addTo(campMap);
|
|
|
|
// Add markers for each camp
|
|
const markers = [];
|
|
validCamps.forEach(camp => {
|
|
const marker = L.marker([camp.lat, camp.lng]).addTo(campMap);
|
|
|
|
// Build popup content with image if available
|
|
let popupContent = '<div style="text-align: center; min-width: 200px;">';
|
|
|
|
if (camp.image) {
|
|
popupContent += `
|
|
<img src="${camp.image}"
|
|
alt="${camp.title}"
|
|
style="width: 100%; max-width: 250px; height: 150px; object-fit: cover; border-radius: 4px; margin-bottom: 8px;"
|
|
onerror="this.style.display='none'">
|
|
`;
|
|
}
|
|
|
|
popupContent += `
|
|
<strong style="font-size: 14px; display: block; margin-bottom: 4px;">${camp.title}</strong>
|
|
<small style="color: #666;">${camp.lat.toFixed(4)}, ${camp.lng.toFixed(4)}</small>
|
|
</div>`;
|
|
|
|
marker.bindPopup(popupContent, {
|
|
maxWidth: 300,
|
|
minWidth: 200
|
|
});
|
|
markers.push(marker);
|
|
});
|
|
|
|
// Fit bounds to show all markers
|
|
if (markers.length > 0) {
|
|
const group = L.featureGroup(markers);
|
|
campMap.fitBounds(group.getBounds().pad(0.1));
|
|
}
|
|
|
|
// Force map to refresh
|
|
setTimeout(() => {
|
|
campMap.invalidateSize();
|
|
}, 100);
|
|
|
|
console.log(`Map initialized with ${validCamps.length} markers`);
|
|
} catch (error) {
|
|
console.error('Error initializing map:', error);
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
function initializeFormHandlers() {
|
|
const form = document.getElementById('campLocationForm');
|
|
form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Uploading images...';
|
|
|
|
try {
|
|
// Upload all pending images first
|
|
await uploadAllPendingImages();
|
|
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
|
|
|
updateJsonData();
|
|
this.submit();
|
|
} catch (error) {
|
|
console.error('Error updating data:', error);
|
|
alert('Failed to process form data. Please try again.');
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Upload all pending images before saving
|
|
async function uploadAllPendingImages() {
|
|
const pendingInputs = document.querySelectorAll('input[type="file"][data-pending-file="true"]');
|
|
|
|
if (pendingInputs.length === 0) {
|
|
return; // No images to upload
|
|
}
|
|
|
|
console.log(`Uploading ${pendingInputs.length} pending images...`);
|
|
|
|
for (const input of pendingInputs) {
|
|
if (!input.files || !input.files[0]) continue;
|
|
|
|
const file = input.files[0];
|
|
const imageType = input.getAttribute('data-image-type') || 'general';
|
|
|
|
try {
|
|
// Create FormData
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
// Upload to server
|
|
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Upload failed');
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Upload failed');
|
|
}
|
|
|
|
// Update the corresponding input field with the uploaded path
|
|
if (input.id === 'heroImageUpload') {
|
|
document.getElementById('heroBackgroundImage').value = result.path;
|
|
} else if (input.dataset.campIndex !== undefined) {
|
|
const campIndex = input.dataset.campIndex;
|
|
const imageInput = document.querySelector(`[name="campImage_${campIndex}"]`);
|
|
if (imageInput) imageInput.value = result.path;
|
|
} else if (input.dataset.locationIndex !== undefined && input.dataset.programIndex === undefined) {
|
|
const locationIndex = input.dataset.locationIndex;
|
|
const imageInput = document.getElementById(`locationImageSrc_${locationIndex}`);
|
|
if (imageInput) imageInput.value = result.path;
|
|
} else if (input.dataset.locationIndex !== undefined && input.dataset.programIndex !== undefined) {
|
|
const locationIndex = input.dataset.locationIndex;
|
|
const programIndex = input.dataset.programIndex;
|
|
const imageInput = document.getElementById(`programImageSrc_${locationIndex}_${programIndex}`);
|
|
if (imageInput) imageInput.value = result.path;
|
|
}
|
|
|
|
// Clear pending flag
|
|
input.dataset.pendingFile = 'false';
|
|
|
|
if (result.reused) {
|
|
console.log(`Reused existing file: ${result.path}`);
|
|
} else {
|
|
console.log(`Uploaded new file: ${result.path}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error uploading image:', error);
|
|
throw new Error(`Failed to upload image: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
console.log('All images uploaded successfully');
|
|
}
|
|
|
|
function updateAllJsonInputs(data) {
|
|
document.getElementById('metadataJson').value = JSON.stringify(data.metadata || {});
|
|
document.getElementById('heroJson').value = JSON.stringify(data.hero || {});
|
|
document.getElementById('campsJson').value = JSON.stringify(data.camps || []);
|
|
document.getElementById('locationsJson').value = JSON.stringify(data.locations || []);
|
|
document.getElementById('locationsSectionJson').value = JSON.stringify(data.locationsSection || {});
|
|
document.getElementById('introJson').value = JSON.stringify(data.intro || {});
|
|
document.getElementById('mapJson').value = JSON.stringify(data.map || {});
|
|
document.getElementById('faqJson').value = JSON.stringify(data.faq || []);
|
|
document.getElementById('faqSectionJson').value = JSON.stringify(data.faqSection || {});
|
|
document.getElementById('welcomeQuoteJson').value = JSON.stringify(data.welcomeQuote || {});
|
|
document.getElementById('securityConceptJson').value = JSON.stringify(data.securityConcept || {});
|
|
}
|
|
|
|
function updateJsonData() {
|
|
// Update Hero
|
|
const heroData = {
|
|
title: document.getElementById('heroTitle').value,
|
|
backgroundImage: document.getElementById('heroBackgroundImage').value
|
|
};
|
|
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
|
|
|
// Update Camps
|
|
const camps = [];
|
|
document.querySelectorAll('.camp-item').forEach((item, index) => {
|
|
const lat = parseFloat(item.querySelector(`[name="campLat_${index}"]`).value);
|
|
const lng = parseFloat(item.querySelector(`[name="campLng_${index}"]`).value);
|
|
const id = parseInt(item.querySelector(`[name="campId_${index}"]`).value);
|
|
camps.push({
|
|
id: isNaN(id) ? 0 : id,
|
|
title: item.querySelector(`[name="campTitle_${index}"]`).value,
|
|
lat: isNaN(lat) ? null : lat,
|
|
lng: isNaN(lng) ? null : lng,
|
|
image: item.querySelector(`[name="campImage_${index}"]`).value
|
|
});
|
|
});
|
|
document.getElementById('campsJson').value = JSON.stringify(camps);
|
|
|
|
// Update Locations
|
|
const locationsSectionData = {
|
|
title: document.getElementById('locationsSectionTitle').value,
|
|
readMoreButtonText: document.getElementById('locationsSectionButtonText').value
|
|
};
|
|
document.getElementById('locationsSectionJson').value = JSON.stringify(locationsSectionData);
|
|
|
|
const locations = [];
|
|
document.querySelectorAll('.location-item').forEach((item, index) => {
|
|
// Collect program options for this location
|
|
const programOptions = [];
|
|
item.querySelectorAll('.program-option-item').forEach((programItem, pIndex) => {
|
|
const title = programItem.querySelector(`[name="programTitle_${index}_${pIndex}"]`)?.value;
|
|
const href = programItem.querySelector(`[name="programHref_${index}_${pIndex}"]`)?.value;
|
|
const imageSrc = programItem.querySelector(`[name="programImageSrc_${index}_${pIndex}"]`)?.value;
|
|
const description = programItem.querySelector(`[name="programDescription_${index}_${pIndex}"]`)?.value;
|
|
|
|
if (title || href) {
|
|
programOptions.push({
|
|
href: href || '',
|
|
imageSrc: imageSrc || '',
|
|
title: title || '',
|
|
description: description || ''
|
|
});
|
|
}
|
|
});
|
|
|
|
const locationId = parseInt(item.querySelector(`[name="locationId_${index}"]`).value);
|
|
locations.push({
|
|
id: isNaN(locationId) ? 0 : locationId,
|
|
country: item.querySelector(`[name="locationCountry_${index}"]`).value,
|
|
title: item.querySelector(`[name="locationTitle_${index}"]`).value,
|
|
imageSrc: item.querySelector(`[name="locationImageSrc_${index}"]`).value,
|
|
imageAlt: item.querySelector(`[name="locationImageAlt_${index}"]`).value,
|
|
readMoreLink: item.querySelector(`[name="locationReadMoreLink_${index}"]`).value,
|
|
imagePosition: item.querySelector(`[name="locationImagePosition_${index}"]`).value,
|
|
cardSize: item.querySelector(`[name="locationCardSize_${index}"]`).value,
|
|
scrollspyClass: item.querySelector(`[name="locationScrollspyClass_${index}"]`)?.value || '',
|
|
programOptions: programOptions
|
|
});
|
|
});
|
|
document.getElementById('locationsJson').value = JSON.stringify(locations);
|
|
|
|
// Update Intro
|
|
const introData = {
|
|
content: document.getElementById('introContent').value
|
|
};
|
|
document.getElementById('introJson').value = JSON.stringify(introData);
|
|
|
|
// Update FAQ
|
|
const faqSectionData = {
|
|
title: document.getElementById('faqSectionTitle').value,
|
|
buttonText: document.getElementById('faqButtonText').value,
|
|
buttonIcon: document.getElementById('faqButtonIcon').value,
|
|
buttonLink: document.getElementById('faqButtonLink').value
|
|
};
|
|
document.getElementById('faqSectionJson').value = JSON.stringify(faqSectionData);
|
|
|
|
const faq = [];
|
|
document.querySelectorAll('.faq-item').forEach((item, index) => {
|
|
faq.push({
|
|
question: item.querySelector(`[name="faqQuestion_${index}"]`).value,
|
|
answer: item.querySelector(`[name="faqAnswer_${index}"]`).value
|
|
});
|
|
});
|
|
document.getElementById('faqJson').value = JSON.stringify(faq);
|
|
|
|
// Update Welcome Quote
|
|
const welcomeQuoteData = {
|
|
title: document.getElementById('welcomeQuoteTitle').value,
|
|
quote: document.getElementById('welcomeQuoteText').value,
|
|
author: document.getElementById('welcomeQuoteAuthor').value
|
|
};
|
|
document.getElementById('welcomeQuoteJson').value = JSON.stringify(welcomeQuoteData);
|
|
|
|
// Update Security Concept
|
|
const securityConceptData = {
|
|
title: document.getElementById('securityConceptTitle').value,
|
|
introduction: document.getElementById('securityConceptIntroduction').value,
|
|
items: []
|
|
};
|
|
document.querySelectorAll('.security-item').forEach((item, index) => {
|
|
securityConceptData.items.push({
|
|
title: item.querySelector(`[name="securityTitle_${index}"]`).value,
|
|
content: item.querySelector(`[name="securityContent_${index}"]`).value
|
|
});
|
|
});
|
|
document.getElementById('securityConceptJson').value = JSON.stringify(securityConceptData);
|
|
|
|
// Update Map
|
|
const mapData = {
|
|
markerTitle: document.getElementById('mapMarkerTitle').value,
|
|
location: document.getElementById('mapLocation').value,
|
|
coordinates: {
|
|
lat: parseFloat(document.getElementById('mapLat').value) || 0,
|
|
lng: parseFloat(document.getElementById('mapLng').value) || 0
|
|
},
|
|
zoom: parseInt(document.getElementById('mapZoom').value) || 15,
|
|
tileLayer: {
|
|
url: document.getElementById('tileLayerUrl').value || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
attribution: document.getElementById('tileLayerAttribution').value || '',
|
|
maxZoom: parseInt(document.getElementById('tileLayerMaxZoom').value) || 18,
|
|
minZoom: parseInt(document.getElementById('tileLayerMinZoom').value) || 0
|
|
}
|
|
};
|
|
document.getElementById('mapJson').value = JSON.stringify(mapData);
|
|
}
|
|
|
|
// Add/Remove functions for Camps
|
|
function addCamp() {
|
|
const container = document.getElementById('campsContainer');
|
|
const index = container.querySelectorAll('.camp-item').length;
|
|
|
|
// Calculate next ID based on existing camps
|
|
let nextId = 1;
|
|
const existingIds = [];
|
|
container.querySelectorAll('.camp-item').forEach((item, idx) => {
|
|
const idInput = item.querySelector(`[name^="campId_"]`);
|
|
if (idInput && idInput.value) {
|
|
existingIds.push(parseInt(idInput.value));
|
|
}
|
|
});
|
|
if (existingIds.length > 0) {
|
|
nextId = Math.max(...existingIds) + 1;
|
|
}
|
|
|
|
// Remove "no camps" alert if exists
|
|
const noDataAlert = container.querySelector('.alert-info');
|
|
if (noDataAlert) {
|
|
noDataAlert.remove();
|
|
}
|
|
|
|
const html = `
|
|
<div class="card mb-3 camp-item border-start border-primary border-3">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<!-- Left Column: Image -->
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-medium">Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control form-control-sm" name="campImage_${index}" value="" placeholder="URL or path">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="document.getElementById('campImageUpload_${index}').click()" data-image-type="camps">
|
|
<i class="fas fa-upload"></i> Upload
|
|
</button>
|
|
</div>
|
|
<input type="file" id="campImageUpload_${index}" accept="image/*" style="display: none;" onchange="handleCampImageUpload(this, ${index})" data-image-type="camps">
|
|
<small class="form-text text-muted d-block mb-2">Recommended size: 800x600px</small>
|
|
<div id="campImagePreview_${index}"></div>
|
|
</div>
|
|
|
|
<!-- Right Column: Fields -->
|
|
<div class="col-md-8">
|
|
<!-- Hidden ID field -->
|
|
<input type="hidden" name="campId_${index}" value="${nextId}">
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label">Title</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" name="campTitle_${index}" value="" placeholder="e.g., Hamburg" id="campTitle_${index}">
|
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="geocodeCampLocation(${index})" title="Find coordinates">
|
|
<i class="fas fa-map-marker-alt"></i> Find
|
|
</button>
|
|
</div>
|
|
<small class="text-muted">Click "Find" to auto-fill coordinates</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Latitude</label>
|
|
<input type="number" step="0.0001" class="form-control" name="campLat_${index}" value="" placeholder="e.g., 53.5597" id="campLat_${index}">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Longitude</label>
|
|
<input type="number" step="0.0001" class="form-control" name="campLng_${index}" value="" placeholder="e.g., 9.9601" id="campLng_${index}">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<div id="geocodeStatus_${index}" class="small"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeCamp(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Camp
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function removeCamp(button) {
|
|
const container = document.getElementById('campsContainer');
|
|
button.closest('.camp-item').remove();
|
|
|
|
// Show "no camps" alert if no camps left
|
|
if (container.querySelectorAll('.camp-item').length === 0) {
|
|
container.innerHTML = `
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>No camps added yet. Click "Add Camp" to create your first camp marker.
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Add/Remove functions for Locations
|
|
function addLocation() {
|
|
const container = document.getElementById('locationsContainer');
|
|
const index = container.querySelectorAll('.location-item').length;
|
|
|
|
// Calculate next ID based on existing locations
|
|
let nextId = 1;
|
|
const existingIds = [];
|
|
container.querySelectorAll('.location-item').forEach((item, idx) => {
|
|
const idInput = item.querySelector(`[name^="locationId_"]`);
|
|
if (idInput && idInput.value) {
|
|
existingIds.push(parseInt(idInput.value));
|
|
}
|
|
});
|
|
if (existingIds.length > 0) {
|
|
nextId = Math.max(...existingIds) + 1;
|
|
}
|
|
|
|
const html = `
|
|
<div class="card mb-3 location-item border-start border-info border-3">
|
|
<div class="card-body">
|
|
<!-- Hidden ID field -->
|
|
<input type="hidden" name="locationId_${index}" value="${nextId}">
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Country</label>
|
|
<input type="text" class="form-control" name="locationCountry_${index}" value="">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label">Title</label>
|
|
<input type="text" class="form-control" name="locationTitle_${index}" value="">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Image Source</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" name="locationImageSrc_${index}" id="locationImageSrc_${index}" value="" placeholder="yootheme/cache/46/image.jpg">
|
|
<button type="button" class="btn btn-outline-primary" onclick="document.getElementById('locationImageUpload_${index}').click()" data-image-type="locations">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<input type="file" id="locationImageUpload_${index}" accept="image/*" style="display: none;" onchange="handleLocationImageUpload(this, ${index})" data-image-type="locations">
|
|
<small class="form-text text-muted">Recommended size: 1200x800px. Upload or enter image path.</small>
|
|
</div>
|
|
<div class="col-md-12" id="locationImagePreviewContainer_${index}">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Image Alt Text</label>
|
|
<input type="text" class="form-control" name="locationImageAlt_${index}" value="">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Image Position</label>
|
|
<select class="form-control" name="locationImagePosition_${index}">
|
|
<option value="left">Left</option>
|
|
<option value="right">Right</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Card Size</label>
|
|
<select class="form-control" name="locationCardSize_${index}">
|
|
<option value="default">Default</option>
|
|
<option value="large">Large</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Read More Link</label>
|
|
<input type="text" class="form-control" name="locationReadMoreLink_${index}" value="">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Scrollspy Class (optional)</label>
|
|
<input type="text" class="form-control" name="locationScrollspyClass_${index}" value="">
|
|
</div>
|
|
|
|
<!-- Program Options -->
|
|
<div class="col-md-12">
|
|
<hr class="my-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label fw-medium mb-0">Program Options</label>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addProgramOption(${index})">
|
|
<i class="fas fa-plus"></i> Add Program
|
|
</button>
|
|
</div>
|
|
<div class="program-options-container" data-location-index="${index}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeLocation(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Location
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function removeLocation(button) {
|
|
button.closest('.location-item').remove();
|
|
}
|
|
|
|
// Add/Remove functions for Program Options
|
|
function addProgramOption(locationIndex) {
|
|
const container = document.querySelector(`.program-options-container[data-location-index="${locationIndex}"]`);
|
|
const pIndex = container.querySelectorAll('.program-option-item').length;
|
|
|
|
const html = `
|
|
<div class="card mb-2 program-option-item bg-light">
|
|
<div class="card-body p-2">
|
|
<div class="row g-2">
|
|
<div class="col-md-6">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Title" name="programTitle_${locationIndex}_${pIndex}" value="">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Link (href)" name="programHref_${locationIndex}_${pIndex}" value="">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Image Source" name="programImageSrc_${locationIndex}_${pIndex}" id="programImageSrc_${locationIndex}_${pIndex}" value="">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="document.getElementById('programImageUpload_${locationIndex}_${pIndex}').click()" data-image-type="programs">
|
|
<i class="fas fa-upload"></i>
|
|
</button>
|
|
</div>
|
|
<input type="file" id="programImageUpload_${locationIndex}_${pIndex}" accept="image/*" style="display: none;" onchange="handleProgramImageUpload(this, ${locationIndex}, ${pIndex})" data-image-type="programs">
|
|
<small class="form-text text-muted">Recommended: 600x400px</small>
|
|
</div>
|
|
<div class="col-md-4" id="programImagePreviewContainer_${locationIndex}_${pIndex}">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control form-control-sm" placeholder="Description" name="programDescription_${locationIndex}_${pIndex}" value="">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeProgramOption(this)">
|
|
<i class="fas fa-times"></i> Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function removeProgramOption(button) {
|
|
button.closest('.program-option-item').remove();
|
|
}
|
|
|
|
// Add/Remove functions for FAQ
|
|
function addFaq() {
|
|
const container = document.getElementById('faqContainer');
|
|
const index = container.querySelectorAll('.faq-item').length;
|
|
const html = `
|
|
<div class="card mb-3 faq-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label">Question</label>
|
|
<input type="text" class="form-control" name="faqQuestion_${index}" value="">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Answer</label>
|
|
<textarea class="form-control" name="faqAnswer_${index}" rows="3"></textarea>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFaq(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove FAQ
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function removeFaq(button) {
|
|
button.closest('.faq-item').remove();
|
|
}
|
|
|
|
// Add/Remove functions for Security Items
|
|
function addSecurityItem() {
|
|
const container = document.getElementById('securityContainer');
|
|
const index = container.querySelectorAll('.security-item').length;
|
|
const html = `
|
|
<div class="card mb-3 security-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label">Title</label>
|
|
<input type="text" class="form-control" name="securityTitle_${index}" value="">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Content</label>
|
|
<textarea class="form-control" name="securityContent_${index}" rows="4"></textarea>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeSecurityItem(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Item
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function removeSecurityItem(button) {
|
|
button.closest('.security-item').remove();
|
|
}
|
|
|
|
// Hero Image Upload and Preview Functions
|
|
function handleHeroImageUpload(input) {
|
|
if (input.files && input.files[0]) {
|
|
const file = input.files[0];
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
// Just show preview, don't upload yet
|
|
showHeroImagePreview(e.target.result);
|
|
|
|
// Store file for later upload
|
|
input.dataset.pendingFile = 'true';
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
function showHeroImagePreview(imageSrc) {
|
|
const container = document.getElementById('heroImagePreviewContainer');
|
|
container.innerHTML = `
|
|
<img src="${imageSrc}" id="heroImagePreview" class="img-thumbnail" style="max-height: 300px; width: 100%; object-fit: cover;" alt="Hero background preview">
|
|
`;
|
|
}
|
|
|
|
// Camp Image Upload and Preview Functions
|
|
function handleCampImageUpload(input, campIndex) {
|
|
if (input.files && input.files[0]) {
|
|
const file = input.files[0];
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
// Just show preview, don't upload yet
|
|
showCampImagePreview(e.target.result, campIndex);
|
|
|
|
// Store file for later upload
|
|
input.dataset.pendingFile = 'true';
|
|
input.dataset.campIndex = campIndex;
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
function showCampImagePreview(imageSrc, campIndex) {
|
|
const previewContainer = document.getElementById(`campImagePreview_${campIndex}`);
|
|
if (previewContainer) {
|
|
previewContainer.innerHTML = `
|
|
<img src="${imageSrc}" class="img-thumbnail" style="width: 100%; max-height: 200px; object-fit: cover;" alt="Camp image preview">
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Location Image Upload and Preview Functions
|
|
function handleLocationImageUpload(input, locationIndex) {
|
|
if (input.files && input.files[0]) {
|
|
const file = input.files[0];
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
// Just show preview, don't upload yet
|
|
showLocationImagePreview(e.target.result, locationIndex);
|
|
|
|
// Store file for later upload
|
|
input.dataset.pendingFile = 'true';
|
|
input.dataset.locationIndex = locationIndex;
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
function showLocationImagePreview(imageSrc, locationIndex) {
|
|
const previewContainer = document.getElementById(`locationImagePreviewContainer_${locationIndex}`);
|
|
if (previewContainer) {
|
|
previewContainer.innerHTML = `
|
|
<img src="${imageSrc}" class="img-thumbnail" style="max-height: 150px; max-width: 300px; object-fit: cover;" alt="Location image preview">
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Program Option Image Upload and Preview Functions
|
|
function handleProgramImageUpload(input, locationIndex, programIndex) {
|
|
if (input.files && input.files[0]) {
|
|
const file = input.files[0];
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
// Just show preview, don't upload yet
|
|
showProgramImagePreview(e.target.result, locationIndex, programIndex);
|
|
|
|
// Store file for later upload
|
|
input.dataset.pendingFile = 'true';
|
|
input.dataset.locationIndex = locationIndex;
|
|
input.dataset.programIndex = programIndex;
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
function showProgramImagePreview(imageSrc, locationIndex, programIndex) {
|
|
const previewContainer = document.getElementById(`programImagePreviewContainer_${locationIndex}_${programIndex}`);
|
|
if (previewContainer) {
|
|
previewContainer.innerHTML = `
|
|
<img src="${imageSrc}" class="img-thumbnail mt-1" style="max-height: 80px; max-width: 120px; object-fit: cover;" alt="Program image preview">
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Reload camp map with current data
|
|
function reloadCampMap() {
|
|
try {
|
|
// Destroy existing map if it exists
|
|
if (campMap) {
|
|
campMap.remove();
|
|
campMap = null;
|
|
}
|
|
|
|
// Re-initialize map
|
|
initializeCampMap();
|
|
} catch (error) {
|
|
console.error('Error reloading map:', error);
|
|
}
|
|
}
|
|
|
|
// Geocoding function for camp locations
|
|
async function geocodeCampLocation(campIndex) {
|
|
const titleInput = document.getElementById(`campTitle_${campIndex}`);
|
|
const latInput = document.getElementById(`campLat_${campIndex}`);
|
|
const lngInput = document.getElementById(`campLng_${campIndex}`);
|
|
const statusDiv = document.getElementById(`geocodeStatus_${campIndex}`);
|
|
|
|
if (!titleInput || !titleInput.value.trim()) {
|
|
statusDiv.innerHTML = '<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> Please enter a location name first</span>';
|
|
return;
|
|
}
|
|
|
|
const locationName = titleInput.value.trim();
|
|
statusDiv.innerHTML = '<span class="text-info"><i class="fas fa-spinner fa-spin"></i> Searching for coordinates...</span>';
|
|
|
|
try {
|
|
// Using Nominatim API (OpenStreetMap)
|
|
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(locationName)}&limit=1`, {
|
|
headers: {
|
|
'User-Agent': 'CampLocationAdmin/1.0'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Geocoding service unavailable');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data && data.length > 0) {
|
|
const result = data[0];
|
|
latInput.value = parseFloat(result.lat).toFixed(4);
|
|
lngInput.value = parseFloat(result.lon).toFixed(4);
|
|
statusDiv.innerHTML = `<span class="text-success"><i class="fas fa-check-circle"></i> Found: ${result.display_name}</span>`;
|
|
|
|
// Reload map to show updated markers
|
|
setTimeout(() => {
|
|
reloadCampMap();
|
|
}, 300);
|
|
} else {
|
|
statusDiv.innerHTML = '<span class="text-danger"><i class="fas fa-times-circle"></i> Location not found. Try a more specific name (e.g., "Hamburg, Germany")</span>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Geocoding error:', error);
|
|
statusDiv.innerHTML = '<span class="text-danger"><i class="fas fa-exclamation-circle"></i> Error finding location. Please try again.</span>';
|
|
}
|
|
}
|
|
|
|
// Update preview when image path is manually changed
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const heroImageInput = document.getElementById('heroBackgroundImage');
|
|
if (heroImageInput) {
|
|
heroImageInput.addEventListener('change', function() {
|
|
if (this.value) {
|
|
showHeroImagePreview(this.value);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|