<div class="flex flex-col gap-6 adjusted-marg-x-sm py-6 bg-white">
<h1 class="h6">Trouver un point de vente près de chez moi</h1>
<div class="flex gap-4">
<div class="relative flex-1" x-data="SearchPlace()" @click.away="showPredictions = false">
<div class="flex gap-3">
<div class="relative flex-1">
<div class="input-wrapper input-wrapper-select relative flex items-center bg-white h-12 overflow-hidden">
<input x-ref="input" type="text" x-model="query" @focus="showPredictions = predictions.length > 0" placeholder="Adresse, code postal..." class="form-select w-full h-full pl-4 pr-12 text-neutral-900 text-base placeholder-neutral-500" />
<button type="button" @click="getLocation()" aria-label="géolocalisez-vous" class="absolute right-3 top-1/2 -translate-y-1/2 btn btn-light btn-solid btn-size-md btn-only-icon">
<svg class=" shrink-0" width="24" height="24" stroke-width="1.5" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="currentColor" stroke-width="1.5" fill="none" />
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="currentColor" stroke-width="1.5" fill="none" />
<path d="M8 0.5V3" stroke="currentColor" stroke-width="1.5" fill="none" />
<path d="M0.5 8H3" stroke="currentColor" stroke-width="1.5" fill="none" />
<path d="M8 15.5V13" stroke="currentColor" stroke-width="1.5" fill="none" />
<path d="M15.5 8H13" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
</button>
</div>
<div x-cloak x-show="showPredictions" class="absolute z-10 top-full left-0 right-0 mt-2 bg-white shadow-md dropdown-content w-full overflow-auto max-h-screen-40" x-transition>
<template x-for="prediction in predictions" :key="prediction.place_id">
<button @click="selectPlace(prediction)" class="dropdown-item cursor-pointer p-2 truncate text-left w-full">
<span x-text="prediction.structured_formatting.main_text + ', ' + prediction.structured_formatting.secondary_text" class="text-neutral-900"></span>
</button>
</template>
</div>
</div>
<button type="button" class=" btn btn-dark btn-outline btn-size-md">
OK
</button>
</div>
</div>
</div>
</div>
<script>
function SearchPlace() {
return {
query: '',
lastSelectedQuery: '',
autocomplete: null,
geocoder: null,
predictions: [],
showPredictions: false,
selectedPlace: null,
isLocating: false,
async init() {
try {
const {
AutocompleteService
} = await google.maps.importLibrary("places");
this.autocomplete = new AutocompleteService();
this.geocoder = new google.maps.Geocoder();
this.getLocation(false);
this.$watch('query', this.debounce(this.handleQueryChange, 300));
} catch (error) {
console.error('Erreur chargement Google Places :', error);
}
},
handleQueryChange(value) {
if (value === this.lastSelectedQuery) {
console.log("lastSelectedQuery OK")
return;
}
console.log("value", value)
if (value.length > 2) {
console.log("getPredictions OK")
this.getPredictions(value);
} else {
console.log("clearPredictions OK")
this.clearPredictions();
}
},
async getPredictions(input) {
try {
const response = await this.autocomplete.getPlacePredictions({
input,
types: ['(regions)'],
});
this.predictions = response.predictions;
console.log(this.predictions.length > 0)
this.showPredictions = this.predictions.length > 0;
} catch (error) {
console.error('Erreur predictions :', error);
}
},
async selectPlace(prediction) {
try {
const response = await this.geocoder.geocode({
placeId: prediction.place_id
});
const result = response.results[0];
const location = result.geometry.location;
const components = result.address_components;
let postalCode = components.find(c => c.types.includes('postal_code'))?.long_name;
const city = components.find(c => c.types.includes('locality'))?.long_name ||
components.find(c => c.types.includes('postal_town'))?.long_name ||
components.find(c => c.types.includes('administrative_area_level_3'))?.long_name ||
components.find(c => c.types.includes('administrative_area_level_2'))?.long_name;
const country = components.find(c => c.types.includes('country'))?.long_name;
// Fallback 1: Try to extract postal code from formatted address
if (!postalCode && result.formatted_address) {
const match = result.formatted_address.match(/\b\d{5}\b/);
if (match) postalCode = match[0];
}
const parts = [];
if (postalCode) parts.push(postalCode);
if (city) parts.push(city);
if (country) parts.push(country);
this.query = parts.join(', ');
this.lastSelectedQuery = this.query;
this.$store.locator.setMapCenter(location.lat(), location.lng(), 12);
this.clearPredictions();
this.$nextTick(() => {
this.$refs.input?.blur();
});
} catch (error) {
console.error('Erreur géolocalisation :', error);
}
},
getLocation(showError = true) {
if (!navigator.geolocation) {
showError && alert('La géolocalisation n\'est pas supportée par ce navigateur.');
return;
}
this.isLocating = true;
navigator.geolocation.getCurrentPosition(
(position) => {
const {
latitude,
longitude
} = position.coords || {};
if (!latitude || !longitude) {
console.warn('Coordonnées GPS vides.');
this.isLocating = false;
return;
}
Alpine.store('locator').setMapCenter(latitude, longitude, 12);
this.isLocating = false;
},
(error) => {
this.isLocating = false;
if (!showError) return;
const messages = {
1: "Veuillez autoriser la géolocalisation dans les paramètres du navigateur.",
2: "Votre position n'est pas disponible actuellement.",
3: "La demande de géolocalisation a expiré."
};
alert(messages[error.code] || "Erreur de géolocalisation inconnue.");
}, {
enableHighAccuracy: true,
timeout: 3000,
maximumAge: 30000
}
);
},
clearPredictions() {
this.showPredictions = false;
this.predictions = [];
},
debounce(func, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
}
}
</script>
<div class="flex flex-col gap-6 adjusted-marg-x-sm py-6 bg-white">
<h1 class="h6">Trouver un point de vente près de chez moi</h1>
<div class="flex gap-4">
<div class="relative flex-1" x-data="SearchPlace()" @click.away="showPredictions = false">
<div class="flex gap-3">
<div class="relative flex-1">
<div class="input-wrapper input-wrapper-select relative flex items-center bg-white h-12 overflow-hidden">
<input
x-ref="input"
type="text"
x-model="query"
@focus="showPredictions = predictions.length > 0"
placeholder="Adresse, code postal..."
class="form-select w-full h-full pl-4 pr-12 text-neutral-900 text-base placeholder-neutral-500"
/>
{% render "@template-button" with {
label: "géolocalisez-vous",
color: "light",
type: "solid",
size: "md",
icon_type: 'only-icon',
icon: {
name:"heroicons--location"
},
custom_button_attrs:[
{
'name': '@click',
'value': 'getLocation()'
}
],
button_class: 'absolute right-3 top-1/2 -translate-y-1/2'
} %}
</div>
<div
x-cloak
x-show="showPredictions"
class="absolute z-10 top-full left-0 right-0 mt-2 bg-white shadow-md dropdown-content w-full overflow-auto max-h-screen-40"
x-transition
>
<template x-for="prediction in predictions" :key="prediction.place_id">
<button
@click="selectPlace(prediction)"
class="dropdown-item cursor-pointer p-2 truncate text-left w-full"
>
<span
x-text="prediction.structured_formatting.main_text + ', ' + prediction.structured_formatting.secondary_text"
class="text-neutral-900"
></span>
</button>
</template>
</div>
</div>
{% render "@template-button" with {
label: "OK",
color: "dark",
type: "outline",
size: "md",
} %}
</div>
</div>
</div>
</div>
<script>
function SearchPlace() {
return {
query: '',
lastSelectedQuery: '',
autocomplete: null,
geocoder: null,
predictions: [],
showPredictions: false,
selectedPlace: null,
isLocating: false,
async init() {
try {
const { AutocompleteService } = await google.maps.importLibrary("places");
this.autocomplete = new AutocompleteService();
this.geocoder = new google.maps.Geocoder();
this.getLocation(false);
this.$watch('query', this.debounce(this.handleQueryChange, 300));
} catch (error) {
console.error('Erreur chargement Google Places :', error);
}
},
handleQueryChange(value) {
if (value === this.lastSelectedQuery) {
console.log("lastSelectedQuery OK")
return;
}
console.log("value", value)
if (value.length > 2) {
console.log("getPredictions OK")
this.getPredictions(value);
} else {
console.log("clearPredictions OK")
this.clearPredictions();
}
},
async getPredictions(input) {
try {
const response = await this.autocomplete.getPlacePredictions({
input,
types: ['(regions)'],
});
this.predictions = response.predictions;
console.log(this.predictions.length > 0)
this.showPredictions = this.predictions.length > 0;
} catch (error) {
console.error('Erreur predictions :', error);
}
},
async selectPlace(prediction) {
try {
const response = await this.geocoder.geocode({ placeId: prediction.place_id });
const result = response.results[0];
const location = result.geometry.location;
const components = result.address_components;
let postalCode = components.find(c => c.types.includes('postal_code'))?.long_name;
const city = components.find(c => c.types.includes('locality'))?.long_name
|| components.find(c => c.types.includes('postal_town'))?.long_name
|| components.find(c => c.types.includes('administrative_area_level_3'))?.long_name
|| components.find(c => c.types.includes('administrative_area_level_2'))?.long_name;
const country = components.find(c => c.types.includes('country'))?.long_name;
// Fallback 1: Try to extract postal code from formatted address
if (!postalCode && result.formatted_address) {
const match = result.formatted_address.match(/\b\d{5}\b/);
if (match) postalCode = match[0];
}
const parts = [];
if (postalCode) parts.push(postalCode);
if (city) parts.push(city);
if (country) parts.push(country);
this.query = parts.join(', ');
this.lastSelectedQuery = this.query;
this.$store.locator.setMapCenter(location.lat(), location.lng(), 12);
this.clearPredictions();
this.$nextTick(() => {
this.$refs.input?.blur();
});
} catch (error) {
console.error('Erreur géolocalisation :', error);
}
},
getLocation(showError = true) {
if (!navigator.geolocation) {
showError && alert('La géolocalisation n\'est pas supportée par ce navigateur.');
return;
}
this.isLocating = true;
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords || {};
if (!latitude || !longitude) {
console.warn('Coordonnées GPS vides.');
this.isLocating = false;
return;
}
Alpine.store('locator').setMapCenter(latitude, longitude, 12);
this.isLocating = false;
},
(error) => {
this.isLocating = false;
if (!showError) return;
const messages = {
1: "Veuillez autoriser la géolocalisation dans les paramètres du navigateur.",
2: "Votre position n'est pas disponible actuellement.",
3: "La demande de géolocalisation a expiré."
};
alert(messages[error.code] || "Erreur de géolocalisation inconnue.");
},
{
enableHighAccuracy: true,
timeout: 3000,
maximumAge: 30000
}
);
},
clearPredictions() {
this.showPredictions = false;
this.predictions = [];
},
debounce(func, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
}
}
</script>
{
"title": "Trouver un point de vente près de chez moi",
"searchPlaceholder": "Adresse, code postal",
"buttonLabel": "Ok",
"detailsLabel": "Détails",
"openLabel": "Ouvert",
"closedLabel": "Fermé",
"untilLabel": "jusqu'à",
"noResults": "Aucun résultat trouvé"
}
No notes defined.