Compare commits

...

3 commits

Author SHA1 Message Date
isUnknown
8e67431622 fix: use title-based slugs for new markers
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 20s
Generate marker slugs from title (e.g., "marqueur-2") instead of
timestamp-based slugs (e.g., "marker-1770362950"). Also fix panelUrl
generation to use Kirby's panel URL method.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 08:47:28 +01:00
isUnknown
575534d182 feat: display custom marker icons on parent map
API now returns iconUrl and iconSize for each marker, allowing custom
marker icons defined in marker.yml to be displayed on the map-editor
field of the parent map page.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 08:28:49 +01:00
isUnknown
68d3142126 fix: marker title displayed immediately on creation
- Return title directly instead of reading from page object
- Prevents showing identifier/slug before reload
- New markers now display "Marqueur [index]" immediately in list
- Also return num directly for consistency

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-29 16:27:06 +01:00
4 changed files with 161 additions and 77 deletions

View file

@ -55,6 +55,15 @@ return [
// Format markers for response // Format markers for response
$markers = []; $markers = [];
foreach ($markerPages as $marker) { foreach ($markerPages as $marker) {
// Get custom icon if available
$iconFile = $marker->markerIcon()->toFile();
$iconUrl = $iconFile ? $iconFile->url() : null;
// Get icon size if set
$iconSize = $marker->markerIconSize()->isNotEmpty()
? (int) $marker->markerIconSize()->value()
: 40;
$markers[] = [ $markers[] = [
'id' => $marker->id(), 'id' => $marker->id(),
'slug' => $marker->slug(), 'slug' => $marker->slug(),
@ -64,7 +73,9 @@ return [
'lon' => (float) $marker->longitude()->value() 'lon' => (float) $marker->longitude()->value()
], ],
'num' => $marker->num(), 'num' => $marker->num(),
'panelUrl' => (string) $marker->panel()->url() 'panelUrl' => (string) $marker->panel()->url(),
'iconUrl' => $iconUrl,
'iconSize' => $iconSize
]; ];
} }
@ -155,15 +166,16 @@ return [
->filterBy('intendedTemplate', 'marker'); ->filterBy('intendedTemplate', 'marker');
$nextNum = $existingMarkers->count() + 1; $nextNum = $existingMarkers->count() + 1;
// Generate unique slug // Generate title and slug based on title
$slug = 'marker-' . time(); $title = 'Marqueur ' . $nextNum;
$slug = Str::slug($title);
// Create the new marker page // Create the new marker page
$newMarker = $mapPage->createChild([ $newMarker = $mapPage->createChild([
'slug' => $slug, 'slug' => $slug,
'template' => 'marker', 'template' => 'marker',
'content' => [ 'content' => [
'title' => 'Marqueur ' . $nextNum, 'title' => $title,
'latitude' => $lat, 'latitude' => $lat,
'longitude' => $lon 'longitude' => $lon
] ]
@ -172,19 +184,28 @@ return [
// Publish the page as listed with the correct num // Publish the page as listed with the correct num
$newMarker->changeStatus('listed', $nextNum); $newMarker->changeStatus('listed', $nextNum);
// Get custom icon if available (new markers won't have one initially)
$iconFile = $newMarker->markerIcon()->toFile();
$iconUrl = $iconFile ? $iconFile->url() : null;
$iconSize = $newMarker->markerIconSize()->isNotEmpty()
? (int) $newMarker->markerIconSize()->value()
: 40;
return [ return [
'status' => 'success', 'status' => 'success',
'data' => [ 'data' => [
'marker' => [ 'marker' => [
'id' => $newMarker->id(), 'id' => $newMarker->id(),
'slug' => $newMarker->slug(), 'slug' => $newMarker->slug(),
'title' => $newMarker->title()->value(), 'title' => $title,
'position' => [ 'position' => [
'lat' => $lat, 'lat' => $lat,
'lon' => $lon 'lon' => $lon
], ],
'num' => $newMarker->num(), 'num' => $nextNum,
'panelUrl' => '/panel/pages/' . $newMarker->id() 'panelUrl' => (string) $newMarker->panel()->url(),
'iconUrl' => $iconUrl,
'iconSize' => $iconSize
] ]
] ]
]; ];
@ -267,6 +288,13 @@ return [
'longitude' => $lon 'longitude' => $lon
]); ]);
// Get custom icon if available
$iconFile = $marker->markerIcon()->toFile();
$iconUrl = $iconFile ? $iconFile->url() : null;
$iconSize = $marker->markerIconSize()->isNotEmpty()
? (int) $marker->markerIconSize()->value()
: 40;
return [ return [
'status' => 'success', 'status' => 'success',
'data' => [ 'data' => [
@ -279,7 +307,9 @@ return [
'lon' => $lon 'lon' => $lon
], ],
'num' => $marker->num(), 'num' => $marker->num(),
'panelUrl' => '/panel/pages/' . $marker->id() 'panelUrl' => (string) $marker->panel()->url(),
'iconUrl' => $iconUrl,
'iconSize' => $iconSize
] ]
] ]
]; ];

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,14 @@
</template> </template>
<script> <script>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'; import {
ref,
computed,
watch,
onMounted,
onBeforeUnmount,
nextTick,
} from 'vue';
import MapPreview from '../map/MapPreview.vue'; import MapPreview from '../map/MapPreview.vue';
import MarkerList from '../map/MarkerList.vue'; import MarkerList from '../map/MarkerList.vue';
import { useMarkersApi } from '../../composables/useMarkersApi.js'; import { useMarkersApi } from '../../composables/useMarkersApi.js';
@ -120,7 +127,8 @@ export default {
}); });
// Initialize API composable (only for multi mode) // Initialize API composable (only for multi mode)
const markersApi = props.mode === 'multi' const markersApi =
props.mode === 'multi'
? useMarkersApi(pageId.value) ? useMarkersApi(pageId.value)
: { markers: ref([]), loading: ref(false), error: ref(null) }; : { markers: ref([]), loading: ref(false), error: ref(null) };
@ -141,14 +149,23 @@ export default {
const lon = singleLon.value; const lon = singleLon.value;
// Only create marker if we have valid coordinates // Only create marker if we have valid coordinates
if (!isNaN(lat) && !isNaN(lon) && lat !== null && lon !== null && lat !== 0 && lon !== 0) { if (
return [{ !isNaN(lat) &&
!isNaN(lon) &&
lat !== null &&
lon !== null &&
lat !== 0 &&
lon !== 0
) {
return [
{
id: 'single-marker', id: 'single-marker',
position: { lat, lon }, position: { lat, lon },
title: 'Current position', title: 'Current position',
iconUrl: props.markerIconUrl, iconUrl: props.markerIconUrl,
iconSize: props.markerIconSize, iconSize: props.markerIconSize,
}]; },
];
} }
return []; return [];
} }
@ -175,7 +192,7 @@ export default {
return { return {
lat: latInput ? parseFloat(latInput.value) : null, lat: latInput ? parseFloat(latInput.value) : null,
lon: lonInput ? parseFloat(lonInput.value) : null lon: lonInput ? parseFloat(lonInput.value) : null,
}; };
} }
@ -194,7 +211,12 @@ export default {
singleLat.value = coords.lat; singleLat.value = coords.lat;
singleLon.value = coords.lon; singleLon.value = coords.lon;
if (!isNaN(coords.lat) && !isNaN(coords.lon) && coords.lat !== 0 && coords.lon !== 0) { if (
!isNaN(coords.lat) &&
!isNaN(coords.lon) &&
coords.lat !== 0 &&
coords.lon !== 0
) {
center.value = { lat: coords.lat, lon: coords.lon }; center.value = { lat: coords.lat, lon: coords.lon };
} }
@ -203,7 +225,11 @@ export default {
if (form) { if (form) {
// Listen to input events // Listen to input events
form.addEventListener('input', (e) => { form.addEventListener('input', (e) => {
if (e.target.name && (e.target.name.includes('latitude') || e.target.name.includes('longitude'))) { if (
e.target.name &&
(e.target.name.includes('latitude') ||
e.target.name.includes('longitude'))
) {
const newCoords = getCoordinatesFromForm(); const newCoords = getCoordinatesFromForm();
singleLat.value = newCoords.lat; singleLat.value = newCoords.lat;
singleLon.value = newCoords.lon; singleLon.value = newCoords.lon;
@ -217,20 +243,32 @@ export default {
if (latInput && lonInput) { if (latInput && lonInput) {
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
const newCoords = getCoordinatesFromForm(); const newCoords = getCoordinatesFromForm();
if (newCoords.lat !== singleLat.value || newCoords.lon !== singleLon.value) { if (
newCoords.lat !== singleLat.value ||
newCoords.lon !== singleLon.value
) {
singleLat.value = newCoords.lat; singleLat.value = newCoords.lat;
singleLon.value = newCoords.lon; singleLon.value = newCoords.lon;
} }
}); });
// Observe attribute changes (value attribute) // Observe attribute changes (value attribute)
observer.observe(latInput, { attributes: true, attributeFilter: ['value'] }); observer.observe(latInput, {
observer.observe(lonInput, { attributes: true, attributeFilter: ['value'] }); attributes: true,
attributeFilter: ['value'],
});
observer.observe(lonInput, {
attributes: true,
attributeFilter: ['value'],
});
// Also poll periodically as a fallback // Also poll periodically as a fallback
const pollInterval = setInterval(() => { const pollInterval = setInterval(() => {
const newCoords = getCoordinatesFromForm(); const newCoords = getCoordinatesFromForm();
if (newCoords.lat !== singleLat.value || newCoords.lon !== singleLon.value) { if (
newCoords.lat !== singleLat.value ||
newCoords.lon !== singleLon.value
) {
singleLat.value = newCoords.lat; singleLat.value = newCoords.lat;
singleLon.value = newCoords.lon; singleLon.value = newCoords.lon;
} }
@ -271,7 +309,14 @@ export default {
([lat, lon]) => { ([lat, lon]) => {
if (props.mode === 'single') { if (props.mode === 'single') {
// Center map on new position if valid // Center map on new position if valid
if (!isNaN(lat) && !isNaN(lon) && lat !== null && lon !== null && lat !== 0 && lon !== 0) { if (
!isNaN(lat) &&
!isNaN(lon) &&
lat !== null &&
lon !== null &&
lat !== 0 &&
lon !== 0
) {
// Force immediate reactivity // Force immediate reactivity
nextTick(() => { nextTick(() => {
if (mapPreview.value && mapPreview.value.centerOnPosition) { if (mapPreview.value && mapPreview.value.centerOnPosition) {
@ -282,11 +327,14 @@ export default {
// Coordinates are invalid/cleared - reset to default center // Coordinates are invalid/cleared - reset to default center
center.value = { center.value = {
lat: props.defaultCenter[0], lat: props.defaultCenter[0],
lon: props.defaultCenter[1] lon: props.defaultCenter[1],
}; };
nextTick(() => { nextTick(() => {
if (mapPreview.value && mapPreview.value.centerOnPosition) { if (mapPreview.value && mapPreview.value.centerOnPosition) {
mapPreview.value.centerOnPosition(center.value.lat, center.value.lon); mapPreview.value.centerOnPosition(
center.value.lat,
center.value.lon
);
} }
}); });
} }
@ -317,7 +365,7 @@ export default {
// Normalize position format (ensure lon, not lng) // Normalize position format (ensure lon, not lng)
const position = { const position = {
lat: currentCenter.lat, lat: currentCenter.lat,
lon: currentCenter.lon || currentCenter.lng lon: currentCenter.lon || currentCenter.lng,
}; };
try { try {
@ -351,7 +399,10 @@ export default {
// Center map on marker // Center map on marker
const marker = markers.value.find((m) => m.id === markerId); const marker = markers.value.find((m) => m.id === markerId);
if (marker && mapPreview.value && mapPreview.value.centerOnPosition) { if (marker && mapPreview.value && mapPreview.value.centerOnPosition) {
mapPreview.value.centerOnPosition(marker.position.lat, marker.position.lon); mapPreview.value.centerOnPosition(
marker.position.lat,
marker.position.lon
);
} }
} }
@ -398,7 +449,7 @@ export default {
function editMarker(markerId) { function editMarker(markerId) {
if (props.mode === 'single') return; if (props.mode === 'single') return;
const marker = markers.value.find(m => m.id === markerId); const marker = markers.value.find((m) => m.id === markerId);
if (marker && marker.panelUrl) { if (marker && marker.panelUrl) {
window.top.location.href = marker.panelUrl; window.top.location.href = marker.panelUrl;
} }

View file

@ -1,4 +1,4 @@
import { ref } from 'vue'; import { ref } from "vue";
/** /**
* Composable for managing markers via Kirby API * Composable for managing markers via Kirby API
@ -29,8 +29,8 @@ export function useMarkersApi(pageId) {
return window.csrf; return window.csrf;
} }
console.warn('CSRF token not found'); console.warn("CSRF token not found");
return ''; return "";
}; };
/** /**
@ -42,24 +42,23 @@ export function useMarkersApi(pageId) {
try { try {
const response = await fetch(`/api/map-editor/pages/${pageId}/markers`, { const response = await fetch(`/api/map-editor/pages/${pageId}/markers`, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}); });
const result = await response.json(); const result = await response.json();
if (result.status === 'error') { if (result.status === "error") {
throw new Error(result.message || 'Failed to fetch markers'); throw new Error(result.message || "Failed to fetch markers");
} }
markers.value = result.data.markers || []; markers.value = result.data.markers || [];
return markers.value; return markers.value;
} catch (err) { } catch (err) {
error.value = err.message; error.value = err.message;
console.error('Error fetching markers:', err); console.error("Error fetching markers:", err);
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@ -75,28 +74,28 @@ export function useMarkersApi(pageId) {
try { try {
const response = await fetch(`/api/map-editor/pages/${pageId}/markers`, { const response = await fetch(`/api/map-editor/pages/${pageId}/markers`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'X-CSRF': getCsrfToken() "X-CSRF": getCsrfToken(),
}, },
body: JSON.stringify({ position }) body: JSON.stringify({ position }),
}); });
const result = await response.json(); const result = await response.json();
if (result.status === 'error') { if (result.status === "error") {
throw new Error(result.message || 'Failed to create marker'); throw new Error(result.message || "Failed to create marker");
} }
const newMarker = result.data.marker; const newMarker = result.data.marker;
markers.value.push(newMarker); markers.value.push(newMarker);
console.log(newMarker);
return newMarker; return newMarker;
} catch (err) { } catch (err) {
error.value = err.message; error.value = err.message;
console.error('Error creating marker:', err); console.error("Error creating marker:", err);
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@ -111,32 +110,34 @@ export function useMarkersApi(pageId) {
error.value = null; error.value = null;
try { try {
const response = await fetch(`/api/map-editor/pages/${pageId}/markers/${markerId}`, { const response = await fetch(
method: 'PATCH', `/api/map-editor/pages/${pageId}/markers/${markerId}`,
{
method: "PATCH",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'X-CSRF': getCsrfToken() "X-CSRF": getCsrfToken(),
}, },
body: JSON.stringify({ position }) body: JSON.stringify({ position }),
}); },
);
const result = await response.json(); const result = await response.json();
if (result.status === 'error') { if (result.status === "error") {
throw new Error(result.message || 'Failed to update marker position'); throw new Error(result.message || "Failed to update marker position");
} }
// Update local marker // Update local marker
const index = markers.value.findIndex(m => m.id === markerId); const index = markers.value.findIndex((m) => m.id === markerId);
if (index !== -1) { if (index !== -1) {
markers.value[index] = result.data.marker; markers.value[index] = result.data.marker;
} }
return result.data.marker; return result.data.marker;
} catch (err) { } catch (err) {
error.value = err.message; error.value = err.message;
console.error('Error updating marker position:', err); console.error("Error updating marker position:", err);
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@ -151,31 +152,33 @@ export function useMarkersApi(pageId) {
error.value = null; error.value = null;
try { try {
const response = await fetch(`/api/map-editor/pages/${pageId}/markers/${markerId}`, { const response = await fetch(
method: 'DELETE', `/api/map-editor/pages/${pageId}/markers/${markerId}`,
{
method: "DELETE",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'X-CSRF': getCsrfToken() "X-CSRF": getCsrfToken(),
} },
}); },
);
const result = await response.json(); const result = await response.json();
if (result.status === 'error') { if (result.status === "error") {
throw new Error(result.message || 'Failed to delete marker'); throw new Error(result.message || "Failed to delete marker");
} }
// Remove from local markers array // Remove from local markers array
const index = markers.value.findIndex(m => m.id === markerId); const index = markers.value.findIndex((m) => m.id === markerId);
if (index !== -1) { if (index !== -1) {
markers.value.splice(index, 1); markers.value.splice(index, 1);
} }
return true; return true;
} catch (err) { } catch (err) {
error.value = err.message; error.value = err.message;
console.error('Error deleting marker:', err); console.error("Error deleting marker:", err);
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@ -189,6 +192,6 @@ export function useMarkersApi(pageId) {
fetchMarkers, fetchMarkers,
createMarker, createMarker,
updateMarkerPosition, updateMarkerPosition,
deleteMarker deleteMarker,
}; };
} }