- Add Active Storage migrations for file attachments - Update Event model to handle image uploads with validation - Replace image URL fields with file upload in forms - Add client-side image preview with validation - Update all views to display uploaded images properly - Fix JSON serialization to prevent stack overflow in API - Add custom image validation methods for format and size - Include image processing variants for different display sizes - Fix promotion code test infrastructure and Stripe configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
701 lines
22 KiB
JavaScript
701 lines
22 KiB
JavaScript
import { Controller } from "@hotwired/stimulus"
|
|
import slug from 'slug'
|
|
|
|
export default class extends Controller {
|
|
static targets = ["name", "slug", "latitude", "longitude", "address", "mapLinksContainer", "geocodingSpinner", "getCurrentLocationBtn", "getCurrentLocationIcon", "getCurrentLocationText", "previewLocationBtn", "previewLocationIcon", "previewLocationText", "messagesContainer"]
|
|
static values = {
|
|
geocodeDelay: { type: Number, default: 1500 } // Delay before auto-geocoding
|
|
}
|
|
|
|
static lastGeocodingRequest = 0
|
|
|
|
connect() {
|
|
this.geocodeTimeout = null
|
|
this.isManualGeocodingInProgress = false
|
|
|
|
// Initialize map links if we have an address and coordinates already exist
|
|
if (this.hasAddressTarget && this.addressTarget.value.trim() &&
|
|
this.hasLatitudeTarget && this.hasLongitudeTarget &&
|
|
this.latitudeTarget.value && this.longitudeTarget.value) {
|
|
this.updateMapLinks()
|
|
}
|
|
}
|
|
|
|
disconnect() {
|
|
if (this.geocodeTimeout) {
|
|
clearTimeout(this.geocodeTimeout)
|
|
}
|
|
}
|
|
|
|
// Generate slug from name
|
|
generateSlug() {
|
|
const name = this.nameTarget.value
|
|
|
|
this.slugTarget.value = slug(name)
|
|
}
|
|
|
|
// Handle address changes with debounced geocoding
|
|
addressChanged() {
|
|
// Clear any existing timeout
|
|
if (this.geocodeTimeout) {
|
|
clearTimeout(this.geocodeTimeout)
|
|
}
|
|
|
|
const address = this.addressTarget.value.trim()
|
|
|
|
if (!address) {
|
|
this.clearCoordinates()
|
|
this.clearMapLinks()
|
|
this.hideGeocodingSpinner()
|
|
return
|
|
}
|
|
|
|
// Show spinner after a brief delay to avoid flickering for very short typing
|
|
const showSpinnerTimeout = setTimeout(() => {
|
|
this.showGeocodingSpinner()
|
|
}, 300)
|
|
|
|
// Debounce geocoding to avoid too many API calls
|
|
this.geocodeTimeout = setTimeout(async () => {
|
|
clearTimeout(showSpinnerTimeout) // Cancel spinner delay if still pending
|
|
this.showGeocodingSpinner() // Show spinner for sure now
|
|
|
|
try {
|
|
await this.geocodeAddressQuiet(address)
|
|
} finally {
|
|
this.hideGeocodingSpinner()
|
|
}
|
|
}, this.geocodeDelayValue)
|
|
}
|
|
|
|
// Get user's current location and reverse geocode to address
|
|
async getCurrentLocation() {
|
|
if (!navigator.geolocation) {
|
|
this.showLocationError("La géolocalisation n'est pas supportée par ce navigateur.")
|
|
return
|
|
}
|
|
|
|
this.showGetCurrentLocationLoading()
|
|
this.showLocationLoading()
|
|
|
|
const options = {
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
maximumAge: 60000
|
|
}
|
|
|
|
try {
|
|
const position = await this.getCurrentPositionPromise(options)
|
|
const lat = position.coords.latitude
|
|
const lng = position.coords.longitude
|
|
|
|
// Set coordinates first
|
|
this.latitudeTarget.value = lat.toFixed(6)
|
|
this.longitudeTarget.value = lng.toFixed(6)
|
|
|
|
// Then reverse geocode to get address
|
|
const address = await this.reverseGeocode(lat, lng)
|
|
|
|
if (address) {
|
|
this.addressTarget.value = address
|
|
this.showLocationSuccess("Position actuelle détectée et adresse mise à jour!")
|
|
} else {
|
|
this.showLocationSuccess("Position actuelle détectée!")
|
|
}
|
|
|
|
this.updateMapLinks()
|
|
this.hideGetCurrentLocationLoading()
|
|
|
|
} catch (error) {
|
|
this.hideGetCurrentLocationLoading()
|
|
this.hideLocationLoading()
|
|
let message = "Erreur lors de la récupération de la localisation."
|
|
|
|
switch(error.code) {
|
|
case error.PERMISSION_DENIED:
|
|
message = "L'accès à la localisation a été refusé."
|
|
break
|
|
case error.POSITION_UNAVAILABLE:
|
|
message = "Les informations de localisation ne sont pas disponibles."
|
|
break
|
|
case error.TIMEOUT:
|
|
message = "La demande de localisation a expiré."
|
|
break
|
|
}
|
|
|
|
this.showLocationError(message)
|
|
} finally {
|
|
this.hideGetCurrentLocationLoading()
|
|
}
|
|
}
|
|
|
|
// Promise wrapper for geolocation
|
|
getCurrentPositionPromise(options) {
|
|
return new Promise((resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(resolve, reject, options)
|
|
})
|
|
}
|
|
|
|
// Reverse geocode coordinates to get address
|
|
async reverseGeocode(lat, lng) {
|
|
try {
|
|
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'User-Agent': 'AperoNight Event Platform/1.0 (https://aperonight.com)',
|
|
'Accept': 'application/json'
|
|
}
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
console.log('Reverse geocoding response:', data) // Debug log
|
|
|
|
if (data && data.display_name) {
|
|
return data.display_name
|
|
}
|
|
|
|
return null
|
|
} catch (error) {
|
|
console.log("Reverse geocoding failed:", error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Preview location - same as updating map links but with user feedback
|
|
previewLocation() {
|
|
if (!this.hasAddressTarget || !this.addressTarget.value.trim()) {
|
|
this.showLocationError("Veuillez saisir une adresse pour la prévisualiser.")
|
|
return
|
|
}
|
|
|
|
// If we already have coordinates, just update map links
|
|
if (this.hasLatitudeTarget && this.hasLongitudeTarget &&
|
|
this.latitudeTarget.value && this.longitudeTarget.value) {
|
|
this.updateMapLinks()
|
|
this.showLocationSuccess("Liens de carte mis à jour!")
|
|
} else {
|
|
// Otherwise geocode the address first
|
|
this.showPreviewLocationLoading()
|
|
this.geocodeAddress().finally(() => {
|
|
this.hidePreviewLocationLoading()
|
|
})
|
|
}
|
|
}
|
|
|
|
// Geocode address manually (with user feedback)
|
|
async geocodeAddress() {
|
|
if (!this.hasAddressTarget || !this.addressTarget.value.trim()) {
|
|
this.showLocationError("Veuillez saisir une adresse.")
|
|
return
|
|
}
|
|
|
|
const address = this.addressTarget.value.trim()
|
|
|
|
try {
|
|
this.isManualGeocodingInProgress = true
|
|
this.showLocationLoading()
|
|
const result = await this.performGeocode(address)
|
|
|
|
if (result) {
|
|
this.latitudeTarget.value = result.lat
|
|
this.longitudeTarget.value = result.lng
|
|
this.updateMapLinks()
|
|
|
|
if (result.accuracy === 'exact') {
|
|
this.showLocationSuccess("Coordonnées exactes trouvées pour cette adresse!")
|
|
} else {
|
|
this.showLocationSuccess(`Coordonnées approximatives trouvées: ${result.display_name}`)
|
|
}
|
|
} else {
|
|
this.showLocationError("Impossible de trouver les coordonnées pour cette adresse.")
|
|
}
|
|
} catch (error) {
|
|
this.showLocationError("Erreur lors de la recherche de l'adresse.")
|
|
} finally {
|
|
this.isManualGeocodingInProgress = false
|
|
this.hideLocationLoading()
|
|
}
|
|
}
|
|
|
|
// Geocode address quietly (no user feedback, for auto-geocoding)
|
|
async geocodeAddressQuiet(address) {
|
|
// Skip if address is too short or invalid
|
|
if (!address || address.length < 5) {
|
|
this.clearCoordinates()
|
|
this.clearMapLinks()
|
|
return
|
|
}
|
|
|
|
try {
|
|
const result = await this.performGeocode(address)
|
|
|
|
if (result && result.lat && result.lng) {
|
|
this.latitudeTarget.value = result.lat
|
|
this.longitudeTarget.value = result.lng
|
|
this.updateMapLinks()
|
|
console.log(`Auto-geocoded "${address}" to ${result.lat}, ${result.lng}`)
|
|
|
|
// Show success message based on accuracy
|
|
if (result.accuracy === 'exact') {
|
|
this.showGeocodingSuccess("Adresse géolocalisée avec précision", result.display_name)
|
|
} else {
|
|
this.showGeocodingSuccess("Adresse géolocalisée approximativement", result.display_name)
|
|
}
|
|
} else {
|
|
// If auto-geocoding fails, show a subtle warning
|
|
this.showGeocodingWarning(address)
|
|
}
|
|
} catch (error) {
|
|
console.log("Auto-geocoding failed:", error)
|
|
this.showGeocodingWarning(address)
|
|
}
|
|
}
|
|
|
|
// Perform the actual geocoding request with fallback strategies
|
|
async performGeocode(address) {
|
|
// Rate limiting: ensure at least 1 second between requests
|
|
const now = Date.now()
|
|
const timeSinceLastRequest = now - (this.constructor.lastGeocodingRequest || 0)
|
|
if (timeSinceLastRequest < 1000) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000 - timeSinceLastRequest))
|
|
}
|
|
this.constructor.lastGeocodingRequest = Date.now()
|
|
|
|
// Try multiple geocoding strategies
|
|
const strategies = [
|
|
// Strategy 1: Exact address
|
|
address,
|
|
// Strategy 2: Street name + city (remove house number)
|
|
address.replace(/^\d+\s*/, ''),
|
|
// Strategy 3: Just city and postal code
|
|
this.extractCityAndPostalCode(address)
|
|
].filter(Boolean) // Remove null/undefined values
|
|
|
|
for (let i = 0; i < strategies.length; i++) {
|
|
const searchAddress = strategies[i]
|
|
console.log(`Geocoding attempt ${i + 1}: "${searchAddress}"`)
|
|
|
|
// Show progress for manual geocoding (not auto-geocoding)
|
|
if (this.isManualGeocodingInProgress) {
|
|
const strategyNames = ['adresse complète', 'rue et ville', 'ville seulement']
|
|
this.showGeocodingProgress(strategyNames[i] || `stratégie ${i + 1}`, `${i + 1}/${strategies.length}`)
|
|
}
|
|
|
|
try {
|
|
const result = await this.tryGeocode(searchAddress)
|
|
if (result) {
|
|
console.log(`Geocoding successful with strategy ${i + 1}`)
|
|
this.hideMessage("geocoding-progress")
|
|
return result
|
|
}
|
|
} catch (error) {
|
|
console.log(`Strategy ${i + 1} failed:`, error.message)
|
|
}
|
|
|
|
// Add small delay between attempts
|
|
if (i < strategies.length - 1) {
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
}
|
|
}
|
|
|
|
this.hideMessage("geocoding-progress")
|
|
|
|
console.log('All geocoding strategies failed')
|
|
return null
|
|
}
|
|
|
|
// Extract city and postal code from address
|
|
extractCityAndPostalCode(address) {
|
|
// Look for French postal code pattern (5 digits) + city
|
|
const match = address.match(/(\d{5})\s+([^,]+)/);
|
|
if (match) {
|
|
return `${match[1]} ${match[2].trim()}`
|
|
}
|
|
|
|
// Fallback: extract last part after comma (assume it's city)
|
|
const parts = address.split(',')
|
|
if (parts.length > 1) {
|
|
return parts[parts.length - 1].trim()
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Try a single geocoding request
|
|
async tryGeocode(address) {
|
|
const encodedAddress = encodeURIComponent(address.trim())
|
|
|
|
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodedAddress}&format=json&limit=1&addressdetails=1`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'User-Agent': 'AperoNight Event Platform/1.0 (https://aperonight.com)',
|
|
'Accept': 'application/json'
|
|
}
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
if (data && data.length > 0) {
|
|
const result = data[0]
|
|
return {
|
|
lat: parseFloat(result.lat).toFixed(6),
|
|
lng: parseFloat(result.lon).toFixed(6),
|
|
display_name: result.display_name,
|
|
accuracy: address === result.display_name ? 'exact' : 'approximate'
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Update map links based on current coordinates
|
|
updateMapLinks() {
|
|
if (!this.hasMapLinksContainerTarget) return
|
|
|
|
const lat = parseFloat(this.latitudeTarget.value)
|
|
const lng = parseFloat(this.longitudeTarget.value)
|
|
const address = this.hasAddressTarget ? this.addressTarget.value.trim() : ""
|
|
|
|
if (isNaN(lat) || isNaN(lng) || !address) {
|
|
this.clearMapLinks()
|
|
return
|
|
}
|
|
|
|
const links = this.generateMapLinks(lat, lng, address)
|
|
this.mapLinksContainerTarget.innerHTML = links
|
|
}
|
|
|
|
// Generate map links HTML
|
|
generateMapLinks(lat, lng, address) {
|
|
const encodedAddress = encodeURIComponent(address)
|
|
|
|
const providers = {
|
|
google: {
|
|
name: "Google Maps",
|
|
url: `https://www.google.com/maps/search/${encodedAddress},16z`,
|
|
icon: "🔍"
|
|
},
|
|
openstreetmap: {
|
|
name: "OpenStreetMap",
|
|
url: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}`,
|
|
icon: "🗺️"
|
|
},
|
|
apple: {
|
|
name: "Apple Plans",
|
|
url: `https://maps.apple.com/?address=${encodedAddress}&ll=${lat},${lng}`,
|
|
icon: "🍎"
|
|
}
|
|
}
|
|
|
|
return `
|
|
<div class="space-y-2">
|
|
<div class="flex items-center space-x-2">
|
|
<i data-lucide="map-pin" class="w-4 h-4 text-gray-500"></i>
|
|
<span class="text-sm font-medium text-gray-700">Voir sur la carte :</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
${Object.entries(providers).map(([key, provider]) => `
|
|
<a href="${provider.url}" target="_blank" rel="noopener"
|
|
class="inline-flex items-center px-3 py-2 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
|
<span class="mr-2">${provider.icon}</span>
|
|
${provider.name}
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
// Clear coordinates
|
|
clearCoordinates() {
|
|
if (this.hasLatitudeTarget) this.latitudeTarget.value = ""
|
|
if (this.hasLongitudeTarget) this.longitudeTarget.value = ""
|
|
}
|
|
|
|
// Clear map links
|
|
clearMapLinks() {
|
|
if (this.hasMapLinksContainerTarget) {
|
|
this.mapLinksContainerTarget.innerHTML = ""
|
|
}
|
|
}
|
|
|
|
// Show geocoding spinner in address input
|
|
showGeocodingSpinner() {
|
|
if (this.hasGeocodingSpinnerTarget) {
|
|
this.geocodingSpinnerTarget.classList.remove('hidden')
|
|
}
|
|
}
|
|
|
|
// Hide geocoding spinner in address input
|
|
hideGeocodingSpinner() {
|
|
if (this.hasGeocodingSpinnerTarget) {
|
|
this.geocodingSpinnerTarget.classList.add('hidden')
|
|
}
|
|
}
|
|
|
|
// Show loading state on "Ma position" button
|
|
showGetCurrentLocationLoading() {
|
|
if (this.hasGetCurrentLocationBtnTarget) {
|
|
this.getCurrentLocationBtnTarget.disabled = true
|
|
}
|
|
if (this.hasGetCurrentLocationIconTarget) {
|
|
this.getCurrentLocationIconTarget.innerHTML = '<div class="w-3 h-3 mr-1 border border-white border-t-transparent rounded-full animate-spin"></div>'
|
|
}
|
|
if (this.hasGetCurrentLocationTextTarget) {
|
|
this.getCurrentLocationTextTarget.textContent = 'Localisation...'
|
|
}
|
|
}
|
|
|
|
// Hide loading state on "Ma position" button
|
|
hideGetCurrentLocationLoading() {
|
|
if (this.hasGetCurrentLocationBtnTarget) {
|
|
this.getCurrentLocationBtnTarget.disabled = false
|
|
}
|
|
if (this.hasGetCurrentLocationIconTarget) {
|
|
this.getCurrentLocationIconTarget.innerHTML = '<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>'
|
|
// Re-initialize Lucide icons
|
|
if (window.lucide) {
|
|
window.lucide.createIcons()
|
|
}
|
|
}
|
|
if (this.hasGetCurrentLocationTextTarget) {
|
|
this.getCurrentLocationTextTarget.textContent = 'Ma position'
|
|
}
|
|
}
|
|
|
|
// Show loading state on "Prévisualiser" button
|
|
showPreviewLocationLoading() {
|
|
if (this.hasPreviewLocationBtnTarget) {
|
|
this.previewLocationBtnTarget.disabled = true
|
|
}
|
|
if (this.hasPreviewLocationIconTarget) {
|
|
this.previewLocationIconTarget.innerHTML = '<div class="w-3 h-3 mr-1 border border-purple-700 border-t-transparent rounded-full animate-spin"></div>'
|
|
}
|
|
if (this.hasPreviewLocationTextTarget) {
|
|
this.previewLocationTextTarget.textContent = 'Recherche...'
|
|
}
|
|
}
|
|
|
|
// Hide loading state on "Prévisualiser" button
|
|
hidePreviewLocationLoading() {
|
|
if (this.hasPreviewLocationBtnTarget) {
|
|
this.previewLocationBtnTarget.disabled = false
|
|
}
|
|
if (this.hasPreviewLocationIconTarget) {
|
|
this.previewLocationIconTarget.innerHTML = '<i data-lucide="map" class="w-3 h-3 mr-1"></i>'
|
|
// Re-initialize Lucide icons
|
|
if (window.lucide) {
|
|
window.lucide.createIcons()
|
|
}
|
|
}
|
|
if (this.hasPreviewLocationTextTarget) {
|
|
this.previewLocationTextTarget.textContent = 'Prévisualiser'
|
|
}
|
|
}
|
|
|
|
// Show loading state
|
|
showLocationLoading() {
|
|
this.hideAllLocationMessages()
|
|
this.showMessage("location-loading", "Géolocalisation en cours...", "loading")
|
|
}
|
|
|
|
// Hide loading state
|
|
hideLocationLoading() {
|
|
this.hideMessage("location-loading")
|
|
}
|
|
|
|
// Show success message
|
|
showLocationSuccess(message) {
|
|
this.hideAllLocationMessages()
|
|
this.showMessage("location-success", message, "success")
|
|
setTimeout(() => this.hideMessage("location-success"), 4000)
|
|
}
|
|
|
|
// Show error message
|
|
showLocationError(message) {
|
|
this.hideAllLocationMessages()
|
|
this.showMessage("location-error", message, "error")
|
|
setTimeout(() => this.hideMessage("location-error"), 6000)
|
|
}
|
|
|
|
// Show geocoding warning (less intrusive than error)
|
|
showGeocodingWarning(address) {
|
|
this.hideMessage("geocoding-warning")
|
|
const message = "Les coordonnées n'ont pas pu être déterminées automatiquement. L'événement utilisera une localisation approximative."
|
|
this.showMessage("geocoding-warning", message, "warning")
|
|
setTimeout(() => this.hideMessage("geocoding-warning"), 8000)
|
|
}
|
|
|
|
// Show info about approximate location
|
|
showApproximateLocationInfo(foundLocation) {
|
|
this.hideMessage("approximate-location-info")
|
|
const message = `Localisation approximative trouvée: ${foundLocation}`
|
|
this.showMessage("approximate-location-info", message, "info")
|
|
setTimeout(() => this.hideMessage("approximate-location-info"), 6000)
|
|
}
|
|
|
|
// Show geocoding success with location details
|
|
showGeocodingSuccess(title, location) {
|
|
this.hideMessage("geocoding-success")
|
|
const message = `${title}<br><small class="opacity-75">${location}</small>`
|
|
this.showMessage("geocoding-success", message, "success")
|
|
setTimeout(() => this.hideMessage("geocoding-success"), 5000)
|
|
}
|
|
|
|
// Show geocoding progress with strategy info
|
|
showGeocodingProgress(strategy, attempt) {
|
|
this.hideMessage("geocoding-progress")
|
|
const message = `Recherche en cours... (${attempt}/${strategy})`
|
|
this.showMessage("geocoding-progress", message, "loading")
|
|
}
|
|
|
|
// Message template configurations
|
|
getMessageTemplate(type) {
|
|
const templates = {
|
|
info: {
|
|
bgColor: "bg-blue-50",
|
|
borderColor: "border-blue-200",
|
|
textColor: "text-blue-800",
|
|
icon: "info",
|
|
iconColor: "text-blue-500"
|
|
},
|
|
success: {
|
|
bgColor: "bg-green-50",
|
|
borderColor: "border-green-200",
|
|
textColor: "text-green-800",
|
|
icon: "check-circle",
|
|
iconColor: "text-green-500"
|
|
},
|
|
error: {
|
|
bgColor: "bg-red-50",
|
|
borderColor: "border-red-200",
|
|
textColor: "text-red-800",
|
|
icon: "alert-circle",
|
|
iconColor: "text-red-500"
|
|
},
|
|
warning: {
|
|
bgColor: "bg-yellow-50",
|
|
borderColor: "border-yellow-200",
|
|
textColor: "text-yellow-800",
|
|
icon: "alert-triangle",
|
|
iconColor: "text-yellow-500"
|
|
},
|
|
loading: {
|
|
bgColor: "bg-purple-50",
|
|
borderColor: "border-purple-200",
|
|
textColor: "text-purple-800",
|
|
icon: "loader-2",
|
|
iconColor: "text-purple-500",
|
|
animated: true
|
|
}
|
|
}
|
|
return templates[type] || templates.info
|
|
}
|
|
|
|
// Create dynamic message HTML using template
|
|
createMessageHTML(id, message, type) {
|
|
const template = this.getMessageTemplate(type)
|
|
const animationClass = template.animated ? 'animate-spin' : ''
|
|
|
|
return `
|
|
<div id="${id}" class="flex items-start space-x-3 p-4 ${template.bgColor} ${template.borderColor} border rounded-lg shadow-sm transition-all duration-200 ease-in-out">
|
|
<div class="flex-shrink-0">
|
|
<i data-lucide="${template.icon}" class="w-5 h-5 ${template.iconColor} ${animationClass}"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium ${template.textColor} leading-relaxed">${message}</p>
|
|
</div>
|
|
<button type="button" onclick="this.parentElement.remove()" class="flex-shrink-0 ${template.textColor} hover:opacity-70 transition-opacity">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
// Show a message with given type using template system
|
|
showMessage(id, message, type) {
|
|
// Remove existing message with same ID first
|
|
this.hideMessage(id)
|
|
|
|
const messageHtml = this.createMessageHTML(id, message, type)
|
|
|
|
// Insert into the dedicated messages container in the venue section
|
|
if (this.hasMessagesContainerTarget) {
|
|
this.messagesContainerTarget.insertAdjacentHTML('beforeend', messageHtml)
|
|
// Re-initialize Lucide icons for the new elements
|
|
if (window.lucide) {
|
|
window.lucide.createIcons()
|
|
}
|
|
} else {
|
|
// Fallback: insert before the address input if messages container not found
|
|
const addressInput = this.hasAddressTarget ? this.addressTarget.parentElement : null
|
|
if (addressInput) {
|
|
addressInput.insertAdjacentHTML('beforebegin', messageHtml)
|
|
if (window.lucide) {
|
|
window.lucide.createIcons()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hide a specific message
|
|
hideMessage(id) {
|
|
const element = document.getElementById(id)
|
|
if (element) {
|
|
element.remove()
|
|
}
|
|
}
|
|
|
|
// Hide all location messages
|
|
hideAllLocationMessages() {
|
|
this.hideMessage("location-loading")
|
|
this.hideMessage("location-success")
|
|
this.hideMessage("location-error")
|
|
this.hideMessage("geocoding-warning")
|
|
this.hideMessage("approximate-location-info")
|
|
this.hideMessage("geocoding-success")
|
|
this.hideMessage("geocoding-progress")
|
|
}
|
|
|
|
// Preview selected image
|
|
previewImage(event) {
|
|
const file = event.target.files[0]
|
|
if (!file) return
|
|
|
|
// Validate file type
|
|
if (!file.type.startsWith('image/')) {
|
|
alert('Veuillez sélectionner une image valide.')
|
|
event.target.value = ''
|
|
return
|
|
}
|
|
|
|
// Validate file size (5MB)
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
alert('L\'image ne doit pas dépasser 5MB.')
|
|
event.target.value = ''
|
|
return
|
|
}
|
|
|
|
// Show preview
|
|
const reader = new FileReader()
|
|
reader.onload = (e) => {
|
|
const previewContainer = document.getElementById('image-preview')
|
|
const previewImg = document.getElementById('preview-img')
|
|
|
|
if (previewContainer && previewImg) {
|
|
previewImg.src = e.target.result
|
|
previewContainer.classList.remove('hidden')
|
|
}
|
|
}
|
|
reader.readAsDataURL(file)
|
|
}
|
|
}
|