<div x-data="googleMap('DEMO_MAP_ID')" x-init="init()" class="flex gap-4 h-[600px]">
    <div class="w-1/3 bg-white shadow rounded-lg overflow-hidden flex flex-col">

        <div class="flex gap-2">
            <button @click="switchStoreType('optique')" :class="storeType === 'optique' ? 'bg-blue-500 text-white' : 'bg-gray-100'" class="px-3 py-1 rounded">
                Optique
            </button>
            <button @click="switchStoreType('acoustique')" :class="storeType === 'acoustique' ? 'bg-blue-500 text-white' : 'bg-gray-100'" class="px-3 py-1 rounded">
                Acoustique
            </button>
        </div>

        <div>
            <div class="p-4 border-b">
                <h2 class="font-medium text-lg">Magasins à proximité</h2>
            </div>
            <div class="divide-y overflow-y-auto flex-1">
                <template x-for="store in paginatedStores">
                    <div class="p-4 hover:bg-gray-50 cursor-pointer" @click="centerOnStore(store)">
                        <h3 class="font-medium" x-text="store[0].name"></h3>
                        <p class="text-sm text-gray-600" x-text="store[0].address + ', ' + store[0].zip + ' ' + store[0].city"></p>
                        <p class="text-sm text-gray-500 mt-1" x-text="formatDistance(store.distance)"></p>
                    </div>
                </template>
            </div>
        </div>

        <div class="p-4 border-t flex justify-between items-center">
            <button @click="previousPage()" :disabled="currentPage === 1" class="px-3 py-1 bg-gray-100 rounded disabled:opacity-50">
                Précédent
            </button>
            <span x-text="`Page ${currentPage}/${totalPages}`"></span>
            <button @click="nextPage()" :disabled="currentPage === totalPages" class="px-3 py-1 bg-gray-100 rounded disabled:opacity-50">
                Suivant
            </button>
        </div>
    </div>
    <div class="flex-1 relative">
        <div id="DEMO_MAP_ID" class="absolute inset-0"></div>
        <template x-if="loading">
            <div class="absolute inset-0 bg-white/80 flex items-center justify-center">
                <div class="loading-spinner"></div>
            </div>
        </template>
    </div>
</div>

<script src="https://maps.googleapis.com/maps/api/js?key=&libraries=geometry,marker"></script>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('googleMap', (mapId) => ({
            storeType: 'optique',
            allStores: [],
            _initialized: false,
            map: null,
            markers: [],
            stores: [],
            sortedStores: [],
            currentInfoWindow: null,
            loading: true,
            currentPage: 1,
            itemsPerPage: 7,
            markerCluster: null,
            updateTimeout: null,
            init() {
                if (this._initialized) return;
                this._initialized = true;
                this.initMap();
            },
            switchStoreType(type) {
                this.storeType = type;
                this.loading = true;
                // Nettoyage complet
                this.clearMarkers();
                this.stores = [];
                this.sortedStores = [];
                // Réinitialisation
                this.filterAndDisplayStores();
                this.updateStoreDistances();
                this.loading = false;
            },
            filterAndDisplayStores() {
                const items = this.allStores.data.items;
                const filteredStores = Object.entries(items).filter(([_, storeArray]) => {
                    const hasAudio = storeArray.some(store => store.is_audio);
                    return this.storeType === 'acoustique' ? hasAudio : !hasAudio;
                });
                this.stores = {
                    success: true,
                    data: {
                        items: Object.fromEntries(filteredStores)
                    }
                };
                Object.values(this.stores.data.items).forEach(store => {
                    this.addMarker(store[0]);
                });
                this.updateVisibleMarkers();
            },
            get paginatedStores() {
                const start = (this.currentPage - 1) * this.itemsPerPage;
                return this.sortedStores.slice(start, start + this.itemsPerPage);
            },
            get totalPages() {
                return Math.ceil(this.sortedStores.length / this.itemsPerPage);
            },
            nextPage() {
                if (this.currentPage < this.totalPages) this.currentPage++;
            },
            previousPage() {
                if (this.currentPage > 1) this.currentPage--;
            },
            async initMap() {
                await this.waitForGoogleMaps();
                this.map = new google.maps.Map(document.getElementById(mapId), {
                    center: {
                        lat: 46.603354,
                        lng: 1.888334
                    },
                    zoom: 6,
                    mapTypeControl: false,
                    fullscreenControl: true,
                    streetViewControl: false
                });
                this.map.addListener('idle', () => {
                    console.log(this.map)
                    if (this.map.manualUpdate) {
                        this.map.manualUpdate = true;
                        return;
                    }
                    clearTimeout(this.updateTimeout);
                    this.updateTimeout = setTimeout(() => {
                        this.updateStoreDistances();
                        this.currentPage = 1;
                    }, 300);
                });
                try {
                    await this.loadStores();
                } finally {
                    this.loading = false;
                }
            },
            waitForGoogleMaps() {
                return new Promise(resolve => {
                    const check = () => {
                        if (window.google?.maps) resolve();
                        else setTimeout(check, 100);
                    };
                    check();
                });
            },
            async loadStores() {
                try {
                    const response = await fetch(`${window.location.origin}/js/json/getList.json`);
                    const data = await response.json();
                    if (!data.success) return;
                    this.allStores = data;
                    this.filterAndDisplayStores();
                    this.updateStoreDistances();
                } catch (error) {
                    console.error('Erreur chargement stores:', error);
                }
            },
            updateVisibleMarkers() {
                const algorithmOptions = new markerClusterer.SuperClusterAlgorithm({
                    radius: 500,
                    maxZoom: 15,
                    minZoom: 3,
                });
                // Créer les clusters
                this.markerCluster = new markerClusterer.MarkerClusterer({
                    map: this.map,
                    markers: this.markers,
                    algorithm: algorithmOptions,
                    renderer: {
                        render: ({
                                count,
                                position
                            }) =>
                            new google.maps.Marker({
                                position,
                                label: {
                                    text: String(count),
                                    color: "white",
                                    fontSize: "14px",
                                    fontWeight: "bold"
                                },
                                icon: {
                                    path: google.maps.SymbolPath.CIRCLE,
                                    fillColor: "#000000",
                                    fillOpacity: 1,
                                    strokeWeight: 0,
                                    scale: count < 10 ? 20 : count < 50 ? 24 : 28
                                }
                            })
                    }
                });
            },
            updateStoreDistances() {
                const center = this.map.getCenter();
                const centerPoint = new google.maps.LatLng(center.lat(), center.lng());
                const stores = Object.values(this.stores.data.items);
                this.sortedStores = stores.map(store => ({
                    ...store,
                    distance: google.maps.geometry.spherical.computeDistanceBetween(
                        centerPoint,
                        new google.maps.LatLng(store[0].lat, store[0].lng)
                    )
                })).sort((a, b) => a.distance - b.distance);
            },
            formatDistance(meters) {
                return meters < 1000 ?
                    `${Math.round(meters)}m` :
                    `${(meters / 1000).toFixed(1)}km`;
            },
            clearMarkers() {
                console.log("clearMarkers this.markerCluster", this.markerCluster)
                console.log("clearMarkers this.markerCluster", this.markerCluster)
                if (this.markerCluster) {
                    this.markerCluster.setMap(null);
                    this.markerCluster.removeMarkers(this.markers)
                    this.markerCluster.clearMarkers(true)
                    this.markerCluster.clearMarkers()
                    this.markerCluster = null;
                }
                this.markers.forEach(marker => {
                    marker.setMap(null);
                    google.maps.event.clearInstanceListeners(marker);
                });
                this.markers = [];
            },
            centerOnStore(store) {
                if (this.currentInfoWindow) {
                    this.currentInfoWindow.close();
                }
                const lat = parseFloat(store[0].lat);
                const lng = parseFloat(store[0].lng);
                this.map.manualUpdate = true;
                this.map.setCenter({
                    lat,
                    lng
                });
                this.map.setZoom(15);
                const marker = this.markers.find(m =>
                    m.getPosition().lat() === lat &&
                    m.getPosition().lng() === lng
                );
                if (marker && this.currentInfoWindow) {
                    this.currentInfoWindow.open(this.map, marker);
                }
            },
            addMarker(store) {
                const lat = parseFloat(store.lat);
                const lng = parseFloat(store.lng);
                if (isNaN(lat) || isNaN(lng)) {
                    console.error('Coordonnées invalides:', store);
                    return;
                }
                try {
                    const marker = new google.maps.Marker({
                        position: {
                            lat,
                            lng
                        },
                        title: store.name,
                        map: null
                    });
                    const infowindow = new google.maps.InfoWindow({
                        content: `
                        <div class="p-2">
                            <h3 class="font-medium">${store.name}</h3>
                            <p class="text-sm">${store.address}, ${store.zip} ${store.city}</p>
                        </div>
                    `
                    });
                    marker.addListener('click', () => {
                        if (this.currentInfoWindow) {
                            this.currentInfoWindow.close();
                        }
                        this.currentInfoWindow = infowindow;
                        infowindow.open(this.map, marker);
                    });
                    this.markers.push(marker);
                } catch (error) {
                    console.error('Erreur création marker:', error);
                }
            },
        }));
    });
</script>
{# components/google-map/google-map.twig #}
<div x-data="googleMap('{{ mapId }}')" x-init="init()" class="flex gap-4 h-[600px]">
	<div class="w-1/3 bg-white shadow rounded-lg overflow-hidden flex flex-col">
		{#		Switcher #}
		<div class="flex gap-2">
			<button @click="switchStoreType('optique')"
					:class="storeType === 'optique' ? 'bg-blue-500 text-white' : 'bg-gray-100'"
					class="px-3 py-1 rounded">
				Optique
			</button>
			<button @click="switchStoreType('acoustique')"
					:class="storeType === 'acoustique' ? 'bg-blue-500 text-white' : 'bg-gray-100'"
					class="px-3 py-1 rounded">
				Acoustique
			</button>
		</div>
		{#		Magasin #}
		<div>
			<div class="p-4 border-b">
				<h2 class="font-medium text-lg">Magasins à proximité</h2>
			</div>
			<div class="divide-y overflow-y-auto flex-1">
				<template x-for="store in paginatedStores">
					<div class="p-4 hover:bg-gray-50 cursor-pointer"
						 @click="centerOnStore(store)">
						<h3 class="font-medium" x-text="store[0].name"></h3>
						<p class="text-sm text-gray-600"
						   x-text="store[0].address + ', ' + store[0].zip + ' ' + store[0].city"></p>
						<p class="text-sm text-gray-500 mt-1" x-text="formatDistance(store.distance)"></p>
					</div>
				</template>
			</div>
		</div>
		{#		Pagination #}
		<div class="p-4 border-t flex justify-between items-center">
			<button @click="previousPage()"
					:disabled="currentPage === 1"
					class="px-3 py-1 bg-gray-100 rounded disabled:opacity-50">
				Précédent
			</button>
			<span x-text="`Page ${currentPage}/${totalPages}`"></span>
			<button @click="nextPage()"
					:disabled="currentPage === totalPages"
					class="px-3 py-1 bg-gray-100 rounded disabled:opacity-50">
				Suivant
			</button>
		</div>
	</div>
	<div class="flex-1 relative">
		<div id="{{ mapId }}" class="absolute inset-0"></div>
		<template x-if="loading">
			<div class="absolute inset-0 bg-white/80 flex items-center justify-center">
				<div class="loading-spinner"></div>
			</div>
		</template>
	</div>
</div>

<script src="https://maps.googleapis.com/maps/api/js?key={{ mapKey }}&libraries=geometry,marker"></script>

<script>
	document.addEventListener('alpine:init', () => {
		Alpine.data('googleMap', (mapId) => ({
			storeType: 'optique',
			allStores: [],
			_initialized: false,
			map: null,
			markers: [],
			stores: [],
			sortedStores: [],
			currentInfoWindow: null,
			loading: true,
			currentPage: 1,
			itemsPerPage: 7,
			markerCluster: null,
			updateTimeout: null,

			init() {
				if (this._initialized) return;
				this._initialized = true;
				this.initMap();
			},

			switchStoreType(type) {
				this.storeType = type;
				this.loading = true;

				// Nettoyage complet
				this.clearMarkers();
				this.stores = [];
				this.sortedStores = [];

				// Réinitialisation
				this.filterAndDisplayStores();
				this.updateStoreDistances();
				this.loading = false;
			},
			filterAndDisplayStores() {
				const items = this.allStores.data.items;
				const filteredStores = Object.entries(items).filter(([_, storeArray]) => {
					const hasAudio = storeArray.some(store => store.is_audio);
					return this.storeType === 'acoustique' ? hasAudio : !hasAudio;
				});

				this.stores = {
					success: true,
					data: {
						items: Object.fromEntries(filteredStores)
					}
				};

				Object.values(this.stores.data.items).forEach(store => {
					this.addMarker(store[0]);
				});

				this.updateVisibleMarkers();
			},

			get paginatedStores() {
				const start = (this.currentPage - 1) * this.itemsPerPage;
				return this.sortedStores.slice(start, start + this.itemsPerPage);
			},
			get totalPages() {
				return Math.ceil(this.sortedStores.length / this.itemsPerPage);
			},
			nextPage() {
				if (this.currentPage < this.totalPages) this.currentPage++;
			},
			previousPage() {
				if (this.currentPage > 1) this.currentPage--;
			},

			async initMap() {
				await this.waitForGoogleMaps();

				this.map = new google.maps.Map(document.getElementById(mapId), {
					center: {lat: 46.603354, lng: 1.888334},
					zoom: 6,
					mapTypeControl: false,
					fullscreenControl: true,
					streetViewControl: false
				});

				this.map.addListener('idle', () => {
					console.log(this.map)
					if (this.map.manualUpdate) {
						this.map.manualUpdate = true;
						return;
					}

					clearTimeout(this.updateTimeout);
					this.updateTimeout = setTimeout(() => {
						this.updateStoreDistances();
						this.currentPage = 1;
					}, 300);
				});

				try {
					await this.loadStores();
				} finally {
					this.loading = false;
				}
			},
			waitForGoogleMaps() {
				return new Promise(resolve => {
					const check = () => {
						if (window.google?.maps) resolve();
						else setTimeout(check, 100);
					};
					check();
				});
			},
			async loadStores() {
				try {
					const response = await fetch(`${window.location.origin}/js/json/getList.json`);
					const data = await response.json();
					if (!data.success) return;

					this.allStores = data;
					this.filterAndDisplayStores();
					this.updateStoreDistances();
				} catch (error) {
					console.error('Erreur chargement stores:', error);
				}
			},

			updateVisibleMarkers() {
				const algorithmOptions = new markerClusterer.SuperClusterAlgorithm({
					radius: 500,
					maxZoom: 15,
					minZoom: 3,
				});

				// Créer les clusters
				this.markerCluster = new markerClusterer.MarkerClusterer({
					map: this.map,
					markers: this.markers,
					algorithm: algorithmOptions,
					renderer: {
						render: ({count, position}) =>
								new google.maps.Marker({
									position,
									label: {text: String(count), color: "white", fontSize: "14px", fontWeight: "bold"},
									icon: {
										path: google.maps.SymbolPath.CIRCLE,
										fillColor: "#000000",
										fillOpacity: 1,
										strokeWeight: 0,
										scale: count < 10 ? 20 : count < 50 ? 24 : 28
									}
								})
					}
				});
			},

			updateStoreDistances() {
				const center = this.map.getCenter();
				const centerPoint = new google.maps.LatLng(center.lat(), center.lng());
				const stores = Object.values(this.stores.data.items);

				this.sortedStores = stores.map(store => ({
					...store,
					distance: google.maps.geometry.spherical.computeDistanceBetween(
							centerPoint,
							new google.maps.LatLng(store[0].lat, store[0].lng)
					)
				})).sort((a, b) => a.distance - b.distance);
			},
			formatDistance(meters) {
				return meters < 1000 ?
						`${Math.round(meters)}m` :
						`${(meters / 1000).toFixed(1)}km`;
			},
			clearMarkers() {
				console.log("clearMarkers this.markerCluster", this.markerCluster)
				console.log("clearMarkers this.markerCluster", this.markerCluster)
				if (this.markerCluster) {
					this.markerCluster.setMap(null);
					this.markerCluster.removeMarkers(this.markers)
					this.markerCluster.clearMarkers(true)
					this.markerCluster.clearMarkers()
					this.markerCluster = null;
				}
				this.markers.forEach(marker => {
					marker.setMap(null);
					google.maps.event.clearInstanceListeners(marker);
				});
				this.markers = [];
			},
			centerOnStore(store) {
				if (this.currentInfoWindow) {
					this.currentInfoWindow.close();
				}

				const lat = parseFloat(store[0].lat);
				const lng = parseFloat(store[0].lng);

				this.map.manualUpdate = true;
				this.map.setCenter({lat, lng});
				this.map.setZoom(15);

				const marker = this.markers.find(m =>
						m.getPosition().lat() === lat &&
						m.getPosition().lng() === lng
				);

				if (marker && this.currentInfoWindow) {
					this.currentInfoWindow.open(this.map, marker);
				}
			},

			addMarker(store) {
				const lat = parseFloat(store.lat);
				const lng = parseFloat(store.lng);

				if (isNaN(lat) || isNaN(lng)) {
					console.error('Coordonnées invalides:', store);
					return;
				}

				try {
					const marker = new google.maps.Marker({
						position: {lat, lng},
						title: store.name,
						map: null
					});

					const infowindow = new google.maps.InfoWindow({
						content: `
                        <div class="p-2">
                            <h3 class="font-medium">${store.name}</h3>
                            <p class="text-sm">${store.address}, ${store.zip} ${store.city}</p>
                        </div>
                    `
					});

					marker.addListener('click', () => {
						if (this.currentInfoWindow) {
							this.currentInfoWindow.close();
						}
						this.currentInfoWindow = infowindow;
						infowindow.open(this.map, marker);
					});

					this.markers.push(marker);
				} catch (error) {
					console.error('Erreur création marker:', error);
				}
			},
		}));
	})
	;
</script>
{
  "mapId": "DEMO_MAP_ID",
  "loading": false
}

No notes defined.