2026-02-21 09:53:31 -05:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onMounted, ref, watch, nextTick, onUnmounted, computed } from "vue";
|
|
|
|
|
|
import { useRouter } from "vue-router";
|
|
|
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
|
|
import { useRouteStore } from "@/stores/route";
|
|
|
|
|
|
import { useMapStore } from "@/stores/map";
|
|
|
|
|
|
import { useBusStopStore } from "@/stores/busStop";
|
|
|
|
|
|
import { useCouponStore } from "@/stores/coupon";
|
|
|
|
|
|
import { useGoogleMaps } from "@/composables/useGoogleMaps";
|
|
|
|
|
|
import { API_URL } from "@/services/apiClient";
|
|
|
|
|
|
import { telemetryService } from "@/services/telemetryService";
|
|
|
|
|
|
import { analyticsService } from "@/services/analyticsService";
|
|
|
|
|
|
|
|
|
|
|
|
import BusStopInfoModal from "@/components/BusStopInfoModal.vue";
|
|
|
|
|
|
import OffersBadge from "@/components/common/OffersBadge.vue";
|
|
|
|
|
|
import type { BusStop } from '@/types'
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
|
|
|
|
|
|
const routeStore = useRouteStore();
|
|
|
|
|
|
const mapStore = useMapStore();
|
|
|
|
|
|
const busStopStore = useBusStopStore();
|
|
|
|
|
|
const couponStore = useCouponStore();
|
|
|
|
|
|
|
|
|
|
|
|
const { map, isLoaded, error: mapsError, initMap, addNumberedMarker, addHtmlMarker, fitBounds, setCenter, setZoom, addMarker, clearAllOverlays } = useGoogleMaps();
|
|
|
|
|
|
const markers = ref<any[]>([]);
|
|
|
|
|
|
const promoMarkers = ref<any[]>([]);
|
|
|
|
|
|
const userMarker = ref<any>(null);
|
|
|
|
|
|
const polyline = ref<google.maps.Polyline | null>(null);
|
|
|
|
|
|
const walkingPolyline = ref<google.maps.Polyline | null>(null);
|
|
|
|
|
|
const walkingPolylineBorder = ref<google.maps.Polyline | null>(null); // Borde blanco estilo Google Maps
|
|
|
|
|
|
const optimalStopPulse = ref<any>(null); // Radar para la parada óptima
|
|
|
|
|
|
const navigationInfo = ref<{ distance: string, duration: string, targetName: string } | null>(null);
|
|
|
|
|
|
const showRouteDropdown = ref(false);
|
|
|
|
|
|
const routeCardRef = ref<HTMLElement | null>(null);
|
|
|
|
|
|
const isUpdatingMarkers = ref(false);
|
|
|
|
|
|
const unitMarkers = ref<Map<string, google.maps.Marker>>(new Map());
|
|
|
|
|
|
const unitFetchInterval = ref<any>(null);
|
|
|
|
|
|
const userCoords = ref<{ lat: number; lng: number } | null>(null); // Store last user location for internal navigation
|
|
|
|
|
|
const currentMarkerMode = ref<'dot' | 'pin' | null>(null);
|
|
|
|
|
|
const mappingSequenceId = ref(0); // Atomic ID to prevent race conditions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Search state
|
|
|
|
|
|
const stopSearchQuery = ref("");
|
|
|
|
|
|
const destinationQuery = ref("");
|
|
|
|
|
|
const originQuery = ref("Mi ubicación");
|
|
|
|
|
|
const filteredSearchResults = ref<BusStop[]>([]);
|
|
|
|
|
|
const showSearchDropdown = ref(false);
|
|
|
|
|
|
const showUberSearch = ref(false);
|
|
|
|
|
|
const showRoutesToggle = ref(false);
|
|
|
|
|
|
const showPromos = ref(false);
|
2026-02-22 13:21:44 -05:00
|
|
|
|
const isInputFocused = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
function onInputFocus() {
|
|
|
|
|
|
isInputFocused.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onInputBlur() {
|
|
|
|
|
|
isInputFocused.value = false;
|
|
|
|
|
|
}
|
2026-02-21 09:53:31 -05:00
|
|
|
|
|
|
|
|
|
|
watch([stopSearchQuery, destinationQuery], ([stopQuery, destQuery]) => {
|
|
|
|
|
|
const query = showUberSearch.value ? destQuery : stopQuery;
|
|
|
|
|
|
if (query.trim().length > 0) {
|
|
|
|
|
|
filteredSearchResults.value = busStopStore.busStops.filter(s =>
|
|
|
|
|
|
s.name.toLowerCase().includes(query.toLowerCase())
|
|
|
|
|
|
).slice(0, 5);
|
|
|
|
|
|
showSearchDropdown.value = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
filteredSearchResults.value = [];
|
|
|
|
|
|
showSearchDropdown.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function selectStopFromSearch(stop: BusStop) {
|
|
|
|
|
|
setCenter(stop.latitude, stop.longitude);
|
|
|
|
|
|
setZoom(17);
|
|
|
|
|
|
handleBusStopClick(stop);
|
|
|
|
|
|
stopSearchQuery.value = "";
|
|
|
|
|
|
destinationQuery.value = "";
|
|
|
|
|
|
showSearchDropdown.value = false;
|
|
|
|
|
|
showUberSearch.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openUberSearch() {
|
|
|
|
|
|
showUberSearch.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeUberSearch() {
|
|
|
|
|
|
showUberSearch.value = false;
|
|
|
|
|
|
destinationQuery.value = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function clearAllMapData() {
|
|
|
|
|
|
console.log('🤖 JARVIS: Iniciando PURGA nuclear con tolerancia a fallos...');
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Respuesta inmediata en UI
|
|
|
|
|
|
showUberSearch.value = false;
|
|
|
|
|
|
showRoutesToggle.value = false;
|
|
|
|
|
|
destinationQuery.value = "";
|
|
|
|
|
|
stopSearchQuery.value = "";
|
|
|
|
|
|
navigationInfo.value = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Invalidar cualquier hilo de dibujo en curso
|
|
|
|
|
|
mappingSequenceId.value++;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 3. Resetear Store
|
|
|
|
|
|
routeStore.clearSelection();
|
|
|
|
|
|
lastProcessedRouteId.value = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Limpieza manual protegida de marcadores
|
|
|
|
|
|
const sweep = (arrayRef: any) => {
|
|
|
|
|
|
if (!arrayRef.value) return;
|
|
|
|
|
|
arrayRef.value.forEach((m: any) => {
|
|
|
|
|
|
try { if (m && m.setMap) m.setMap(null); } catch (e) {}
|
|
|
|
|
|
});
|
|
|
|
|
|
arrayRef.value = [];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
sweep(markers);
|
|
|
|
|
|
sweep(promoMarkers);
|
|
|
|
|
|
|
|
|
|
|
|
// Limpiar Unidades de transporte
|
|
|
|
|
|
if (unitMarkers.value) {
|
|
|
|
|
|
unitMarkers.value.forEach((m: any) => {
|
|
|
|
|
|
try { if (m && m.setMap) m.setMap(null); } catch (e) {}
|
|
|
|
|
|
});
|
|
|
|
|
|
unitMarkers.value.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. Barrido profundo de Google
|
|
|
|
|
|
if (typeof clearAllOverlays === 'function') {
|
|
|
|
|
|
try { clearAllOverlays(); } catch (e) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. Limpiar polilíneas y pulsos
|
|
|
|
|
|
if (polyline.value) { polyline.value.setMap(null); polyline.value = null; }
|
|
|
|
|
|
if (walkingPolyline.value) { walkingPolyline.value.setMap(null); walkingPolyline.value = null; }
|
|
|
|
|
|
if (optimalStopPulse.value) {
|
|
|
|
|
|
try { if (optimalStopPulse.value.setMap) optimalStopPulse.value.setMap(null); } catch(e){}
|
|
|
|
|
|
optimalStopPulse.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7. Restaurar Solo Usuario tras un breve respiro
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
if (userCoords.value) {
|
|
|
|
|
|
const { lat, lng } = userCoords.value;
|
|
|
|
|
|
if (userMarker.value && userMarker.value.setMap) {
|
|
|
|
|
|
try { userMarker.value.setMap(null); } catch(e){}
|
|
|
|
|
|
}
|
|
|
|
|
|
userMarker.value = addHtmlMarker({ lat, lng }, sonarHtml, { x: -30, y: -30 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('🤖 JARVIS: Purga completada con éxito.');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('❌ JARVIS: Error crítico en purga, pero el mapa debería estar limpio:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Modal state
|
|
|
|
|
|
const showBusStopModal = ref(false);
|
|
|
|
|
|
const selectedBusStop = ref<BusStop | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const showPromoModal = ref(false);
|
|
|
|
|
|
const selectedPromo = ref<any>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// Close dropdown when clicking outside
|
|
|
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
|
|
|
|
if (routeCardRef.value && !routeCardRef.value.contains(event.target as Node)) {
|
|
|
|
|
|
showRouteDropdown.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleBusStopClick(stop: BusStop) {
|
|
|
|
|
|
selectedBusStop.value = stop;
|
|
|
|
|
|
showBusStopModal.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeBusStopModal() {
|
|
|
|
|
|
showBusStopModal.value = false;
|
|
|
|
|
|
selectedBusStop.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePromoClick(promo: any) {
|
|
|
|
|
|
selectedPromo.value = promo;
|
|
|
|
|
|
showPromoModal.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closePromoModal() {
|
|
|
|
|
|
showPromoModal.value = false;
|
|
|
|
|
|
selectedPromo.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getImageUrl(path: string | null | undefined) {
|
|
|
|
|
|
if (!path) return '/default-coupon.png'
|
|
|
|
|
|
if (path.startsWith('http')) return path
|
|
|
|
|
|
return `${API_URL}${path.startsWith('/') ? '' : '/'}${path}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function claimPromo() {
|
|
|
|
|
|
if (!selectedPromo.value) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await couponStore.claimCoupon(selectedPromo.value.id)
|
|
|
|
|
|
alert('¡Promoción reclamada con éxito! Revisa "Mis Cupones" en tu perfil.')
|
|
|
|
|
|
closePromoModal()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
alert(e.message || 'Error al reclamar la promoción')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'Map' })
|
|
|
|
|
|
// Add click outside listener
|
|
|
|
|
|
document.addEventListener('click', handleClickOutside);
|
|
|
|
|
|
|
|
|
|
|
|
// Load routes, bus stops and promos
|
|
|
|
|
|
await routeStore.loadRoutes();
|
|
|
|
|
|
await couponStore.loadCoupons({ active_only: true });
|
|
|
|
|
|
|
|
|
|
|
|
// Sync from query params if coming from Schedules or external link
|
|
|
|
|
|
const queryRouteId = router.currentRoute.value.query.routeId as string;
|
|
|
|
|
|
if (queryRouteId && queryRouteId !== routeStore.selectedRouteId) {
|
|
|
|
|
|
const foundRoute = routeStore.allRoutes.find(r => r.id === queryRouteId);
|
|
|
|
|
|
if (foundRoute) {
|
|
|
|
|
|
// Use selectRoute to load stops and update store
|
|
|
|
|
|
await routeStore.selectRoute(foundRoute.id, foundRoute.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Wait for Google Maps to load
|
|
|
|
|
|
if (isLoaded.value) {
|
|
|
|
|
|
await initializeMap();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Watch for when maps are loaded
|
|
|
|
|
|
const unwatch = watch(isLoaded, async (loaded) => {
|
|
|
|
|
|
if (loaded) {
|
|
|
|
|
|
await initializeMap();
|
|
|
|
|
|
unwatch();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start periodic fetch of active units
|
|
|
|
|
|
unitFetchInterval.value = setInterval(updateActiveUnits, 15000);
|
|
|
|
|
|
updateActiveUnits();
|
|
|
|
|
|
|
|
|
|
|
|
// Carousel auto-slide
|
|
|
|
|
|
startCarousel();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const currentCarouselIndex = ref(0);
|
|
|
|
|
|
const currentPromo = computed(() => {
|
|
|
|
|
|
if (couponStore.coupons.length === 0) return null;
|
|
|
|
|
|
// Ensure we don't exceed bounds
|
|
|
|
|
|
const idx = currentCarouselIndex.value % couponStore.coupons.length;
|
|
|
|
|
|
return couponStore.coupons[idx];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const carouselTimer = ref<any>(null);
|
|
|
|
|
|
|
|
|
|
|
|
function startCarousel() {
|
|
|
|
|
|
if (carouselTimer.value) clearInterval(carouselTimer.value);
|
|
|
|
|
|
carouselTimer.value = setInterval(() => {
|
|
|
|
|
|
if (couponStore.coupons.length > 0) {
|
|
|
|
|
|
currentCarouselIndex.value = (currentCarouselIndex.value + 1) % couponStore.coupons.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopCarousel() {
|
|
|
|
|
|
if (carouselTimer.value) clearInterval(carouselTimer.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function nextPromo() {
|
|
|
|
|
|
currentCarouselIndex.value = (currentCarouselIndex.value + 1) % couponStore.coupons.length;
|
|
|
|
|
|
startCarousel();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function prevPromo() {
|
|
|
|
|
|
currentCarouselIndex.value = (currentCarouselIndex.value - 1 + couponStore.coupons.length) % couponStore.coupons.length;
|
|
|
|
|
|
startCarousel();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
document.removeEventListener('click', handleClickOutside);
|
|
|
|
|
|
if (unitFetchInterval.value) clearInterval(unitFetchInterval.value);
|
|
|
|
|
|
if (carouselTimer.value) clearInterval(carouselTimer.value);
|
|
|
|
|
|
// Clear all markers when component unmounts
|
|
|
|
|
|
clearMapMarkers();
|
|
|
|
|
|
|
|
|
|
|
|
// Clear unit markers
|
|
|
|
|
|
unitMarkers.value.forEach(m => m.setMap(null));
|
|
|
|
|
|
unitMarkers.value.clear();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async function initializeMap() {
|
|
|
|
|
|
// Wait for DOM to be ready
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
|
|
|
|
// Small delay to ensure the element is rendered
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
|
|
|
|
|
|
|
|
initMap("map", mapStore.center, mapStore.zoom);
|
|
|
|
|
|
|
|
|
|
|
|
if (map.value) {
|
|
|
|
|
|
map.value.addListener('zoom_changed', () => {
|
|
|
|
|
|
updateMarkersStyles();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If we have a selected route, show its stops
|
|
|
|
|
|
if (routeStore.selectedRouteId && routeStore.selectedRouteStops.length > 0) {
|
|
|
|
|
|
updateMapMarkers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show promotions on the map
|
|
|
|
|
|
updatePromoMarkers();
|
|
|
|
|
|
|
|
|
|
|
|
// Apply initial styles based on current zoom
|
|
|
|
|
|
updateMarkersStyles(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Watch for route selection changes
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => routeStore.selectedRouteId,
|
|
|
|
|
|
async (routeId, oldRouteId) => {
|
|
|
|
|
|
// ALWAYS clear markers first when route changes - do this immediately
|
|
|
|
|
|
if (oldRouteId !== routeId) {
|
|
|
|
|
|
console.log(`Route changing from ${oldRouteId} to ${routeId} - clearing markers`)
|
|
|
|
|
|
lastProcessedRouteId.value = routeId; // Update before clearing
|
|
|
|
|
|
clearMapMarkers();
|
|
|
|
|
|
// Wait a bit to ensure markers are fully removed
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 150));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (routeId) {
|
|
|
|
|
|
// Route stops are automatically loaded when route is selected
|
|
|
|
|
|
// Update map markers for the selected route
|
|
|
|
|
|
await updateMapMarkers();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Clear markers when no route is selected
|
|
|
|
|
|
lastProcessedRouteId.value = null;
|
|
|
|
|
|
clearMapMarkers();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: false }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Track the last route ID to prevent double updates
|
|
|
|
|
|
const lastProcessedRouteId = ref<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// Watch for route stops changes - but only if route ID hasn't changed
|
|
|
|
|
|
// This prevents double updates when both watchers fire
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => routeStore.selectedRouteStops,
|
|
|
|
|
|
async (newStops, oldStops) => {
|
|
|
|
|
|
const currentRouteId = routeStore.selectedRouteId;
|
|
|
|
|
|
|
|
|
|
|
|
// Skip if route ID was just changed (the routeId watcher will handle it)
|
|
|
|
|
|
if (currentRouteId === lastProcessedRouteId.value) {
|
|
|
|
|
|
// Only update if route is selected and map is loaded
|
|
|
|
|
|
// Skip if we're already updating or if stops haven't actually changed
|
|
|
|
|
|
if (currentRouteId && isLoaded.value && !isUpdatingMarkers.value) {
|
|
|
|
|
|
// Check if stops actually changed
|
|
|
|
|
|
if (!oldStops || newStops.length !== oldStops.length ||
|
|
|
|
|
|
newStops.some((stop, idx) => stop.id !== oldStops[idx]?.id)) {
|
|
|
|
|
|
console.log('Route stops changed - updating markers')
|
|
|
|
|
|
await updateMapMarkers();
|
|
|
|
|
|
|
|
|
|
|
|
// FLOW REFINEMENT: After markers are loaded, find the optimal entrance stop
|
|
|
|
|
|
if (newStops.length > 0) {
|
|
|
|
|
|
highlightOptimalStopForRoute();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
function clearMapMarkers() {
|
|
|
|
|
|
console.log('clearMapMarkers called - clearing bus stop markers')
|
|
|
|
|
|
|
|
|
|
|
|
// Do NOT call clearAllOverlays() here as it wipes out EVERYTHING (including POIs)
|
|
|
|
|
|
// Instead, clear only the markers we track locally for routes
|
|
|
|
|
|
|
|
|
|
|
|
// Also clear our local tracking and ensure markers are removed
|
|
|
|
|
|
const markerCount = markers.value.length
|
|
|
|
|
|
markers.value.forEach((marker: any) => {
|
|
|
|
|
|
if (marker) {
|
|
|
|
|
|
// Remove marker from map
|
|
|
|
|
|
if (marker.setMap) {
|
|
|
|
|
|
marker.setMap(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Also try to remove it if it has a remove method
|
|
|
|
|
|
if (typeof marker.remove === 'function') {
|
|
|
|
|
|
marker.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// Clear the array
|
|
|
|
|
|
markers.value = [];
|
|
|
|
|
|
console.log(`Cleared ${markerCount} local markers`)
|
|
|
|
|
|
|
|
|
|
|
|
// Clear polyline
|
|
|
|
|
|
if (polyline.value) {
|
|
|
|
|
|
polyline.value.setMap(null);
|
|
|
|
|
|
polyline.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clear walking polyline
|
|
|
|
|
|
if (walkingPolyline.value) {
|
|
|
|
|
|
walkingPolyline.value.setMap(null);
|
|
|
|
|
|
walkingPolyline.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (walkingPolylineBorder.value) {
|
|
|
|
|
|
walkingPolylineBorder.value.setMap(null);
|
|
|
|
|
|
walkingPolylineBorder.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clear navigation info
|
|
|
|
|
|
navigationInfo.value = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Clear optimal pulse
|
|
|
|
|
|
if (optimalStopPulse.value) {
|
|
|
|
|
|
if (typeof optimalStopPulse.value.setMap === 'function') {
|
|
|
|
|
|
optimalStopPulse.value.setMap(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
optimalStopPulse.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function updateMapMarkers() {
|
|
|
|
|
|
if (!isLoaded.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Incrementar ID de secuencia para invalidar dibujos previos
|
|
|
|
|
|
mappingSequenceId.value++;
|
|
|
|
|
|
const thisSeq = mappingSequenceId.value;
|
|
|
|
|
|
|
|
|
|
|
|
const currentRequestRouteId = routeStore.selectedRouteId;
|
|
|
|
|
|
const stops = [...routeStore.selectedRouteStops];
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentRequestRouteId || stops.length === 0) {
|
|
|
|
|
|
clearMapMarkers();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isUpdatingMarkers.value = true;
|
|
|
|
|
|
console.log(`🤖 JARVIS: Iniciando dibujo de markers (Secuencia: ${thisSeq})`)
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 30));
|
|
|
|
|
|
|
|
|
|
|
|
// Abortar si la secuencia cambió durante la espera
|
|
|
|
|
|
if (mappingSequenceId.value !== thisSeq || !routeStore.selectedRouteId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newMarkers: any[] = [];
|
|
|
|
|
|
const path: Array<{ lat: number; lng: number }> = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < stops.length; i++) {
|
|
|
|
|
|
const stop = stops[i];
|
|
|
|
|
|
if (!stop) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Verificación atómica en cada paso
|
|
|
|
|
|
if (mappingSequenceId.value !== thisSeq || !routeStore.selectedRouteId) {
|
|
|
|
|
|
newMarkers.forEach(m => { if (m.setMap) m.setMap(null); });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const marker = addNumberedMarker(
|
|
|
|
|
|
{ lat: stop.latitude, lng: stop.longitude },
|
|
|
|
|
|
i + 1,
|
|
|
|
|
|
stop.name,
|
|
|
|
|
|
() => handleBusStopClick(stop)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (marker) newMarkers.push(marker);
|
|
|
|
|
|
path.push({ lat: stop.latitude, lng: stop.longitude });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Final check before committing to the map
|
|
|
|
|
|
if (mappingSequenceId.value !== thisSeq || !routeStore.selectedRouteId) {
|
|
|
|
|
|
newMarkers.forEach(m => { if (m.setMap) m.setMap(null); });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearMapMarkers();
|
|
|
|
|
|
markers.value = newMarkers;
|
|
|
|
|
|
|
|
|
|
|
|
if (path.length > 0) fitBounds(path);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('❌ JARVIS: Error en updateMapMarkers:', err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (mappingSequenceId.value === thisSeq) {
|
|
|
|
|
|
isUpdatingMarkers.value = false;
|
|
|
|
|
|
if (routeStore.selectedRouteId) updateMarkersStyles(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Optimización de rendimiento: Solo actualiza los iconos si cambiamos de modo (punto vs pin)
|
|
|
|
|
|
* o si se fuerza la actualización (ej: al cargar nueva ruta)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function updateMarkersStyles(force = false) {
|
|
|
|
|
|
if (!map.value || markers.value.length === 0 || !routeStore.selectedRouteId) return;
|
|
|
|
|
|
|
|
|
|
|
|
const currentZoom = map.value.getZoom() || 12;
|
|
|
|
|
|
const newMode = currentZoom >= 15 ? 'pin' : 'dot';
|
|
|
|
|
|
|
|
|
|
|
|
if (!force && currentMarkerMode.value === newMode) return;
|
|
|
|
|
|
|
|
|
|
|
|
currentMarkerMode.value = newMode;
|
|
|
|
|
|
const showNumbers = newMode === 'pin';
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🤖 JARVIS: Actualizando estilos de marcadores a modo: ${newMode}`);
|
|
|
|
|
|
|
|
|
|
|
|
markers.value.forEach((marker: any, index: number) => {
|
|
|
|
|
|
if (!marker) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Si la secuencia cambió o la ruta desapareció mientras hacíamos esto, abortamos
|
|
|
|
|
|
if (!routeStore.selectedRouteId) {
|
|
|
|
|
|
if (marker.setMap) marker.setMap(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (showNumbers) {
|
|
|
|
|
|
// MODO PREMIUM: Círculo Amarillo con Borde Negro y Numero Negro
|
|
|
|
|
|
marker.setIcon({
|
|
|
|
|
|
path: google.maps.SymbolPath.CIRCLE,
|
|
|
|
|
|
fillColor: '#FEE715',
|
|
|
|
|
|
fillOpacity: 1,
|
|
|
|
|
|
strokeColor: '#101820',
|
|
|
|
|
|
strokeWeight: 2.5,
|
|
|
|
|
|
scale: 14, // Tamaño ideal para leer el número dentro
|
|
|
|
|
|
});
|
|
|
|
|
|
marker.setLabel({
|
|
|
|
|
|
text: (index + 1).toString(),
|
|
|
|
|
|
color: '#101820',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
fontWeight: '900',
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// MODO COMPACTO: Punto Amarillo brillante con anillo de profundidad
|
|
|
|
|
|
marker.setIcon({
|
|
|
|
|
|
path: google.maps.SymbolPath.CIRCLE,
|
|
|
|
|
|
fillColor: '#FEE715',
|
|
|
|
|
|
fillOpacity: 1,
|
|
|
|
|
|
strokeColor: '#101820',
|
|
|
|
|
|
strokeWeight: 1.5,
|
|
|
|
|
|
scale: 7,
|
|
|
|
|
|
});
|
|
|
|
|
|
marker.setLabel(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// La función drawRouteOnRoad ha sido eliminada por petición del usuario (línea azul removida)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function updatePromoMarkers() {
|
|
|
|
|
|
if (!isLoaded.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// ALWAYS clear existing promo markers first
|
|
|
|
|
|
promoMarkers.value.forEach(m => m.setMap(null));
|
|
|
|
|
|
promoMarkers.value = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Only show coupons that have a business with coordinates
|
|
|
|
|
|
const promosWithCoords = couponStore.coupons.filter(c =>
|
|
|
|
|
|
c.is_active && c.business && c.business.latitude && c.business.longitude
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`Adding ${promosWithCoords.length} promo markers`);
|
|
|
|
|
|
|
|
|
|
|
|
promosWithCoords.forEach(promo => {
|
|
|
|
|
|
const marker = addMarker(
|
|
|
|
|
|
{ lat: promo.business!.latitude!, lng: promo.business!.longitude! },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: promo.title,
|
|
|
|
|
|
icon: {
|
|
|
|
|
|
path: "M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.65-.5-.65C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z",
|
|
|
|
|
|
fillColor: '#FF4081', // Pinkish/Red for promos
|
|
|
|
|
|
fillOpacity: 1,
|
|
|
|
|
|
strokeColor: '#FFFFFF',
|
|
|
|
|
|
strokeWeight: 2,
|
|
|
|
|
|
anchor: new google.maps.Point(12, 12),
|
|
|
|
|
|
scale: 2
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
if (marker) {
|
|
|
|
|
|
marker.addListener('click', () => handlePromoClick(promo));
|
|
|
|
|
|
promoMarkers.value.push(marker);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectRouteAndClose(routeId: string, routeName: string) {
|
|
|
|
|
|
console.log(`🤖 JARVIS: Iniciando viaje hacia ${routeName}`);
|
|
|
|
|
|
routeStore.selectRoute(routeId, routeName);
|
|
|
|
|
|
showRouteDropdown.value = false;
|
|
|
|
|
|
showUberSearch.value = false; // Close the expanded search panel
|
|
|
|
|
|
|
|
|
|
|
|
// Si no tenemos ubicación, la pedimos para poder calcular la parada óptima automáticamente
|
|
|
|
|
|
if (!userCoords.value) {
|
|
|
|
|
|
locateUser();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
async function updateActiveUnits() {
|
|
|
|
|
|
if (!isLoaded.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const units = await telemetryService.getActiveUnits();
|
|
|
|
|
|
console.log(`Fetched ${units.length} active units`);
|
|
|
|
|
|
|
|
|
|
|
|
// Create a set of current unit IDs to handle removals
|
|
|
|
|
|
const currentUnitIds = new Set(units.map(u => u.user_id));
|
|
|
|
|
|
|
|
|
|
|
|
// Remove markers for units that are no longer active
|
|
|
|
|
|
for (const [userId, marker] of unitMarkers.value.entries()) {
|
|
|
|
|
|
if (!currentUnitIds.has(userId)) {
|
|
|
|
|
|
marker.setMap(null);
|
|
|
|
|
|
unitMarkers.value.delete(userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update or add markers for active units
|
|
|
|
|
|
units.forEach(unit => {
|
|
|
|
|
|
const existingMarker = unitMarkers.value.get(unit.user_id);
|
|
|
|
|
|
const position = { lat: unit.latitude, lng: unit.longitude };
|
|
|
|
|
|
|
|
|
|
|
|
if (existingMarker) {
|
|
|
|
|
|
// Move existing marker
|
|
|
|
|
|
existingMarker.setPosition(position);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Create new unit marker
|
|
|
|
|
|
const isBus = unit.vehicle_type === 'bus';
|
|
|
|
|
|
const marker = addMarker(position, {
|
|
|
|
|
|
title: `${unit.full_name} (${unit.license_plate})`,
|
|
|
|
|
|
icon: {
|
|
|
|
|
|
path: isBus ? "M20 12h-2V4c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v15c0 1.1.9 2 2 2h1c0 1.1.9 2 2 2s2-.9 2-2h6c0 1.1.9 2 2 2s2-.9 2-2h1c1.1 0 2-.9 2-2v-7c0-1.1-.9-2-2-2zM6 19c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm12 0c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm2-5h-2v-2h2v2z" : "M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z",
|
|
|
|
|
|
fillColor: isBus ? '#1976d2' : '#FEE715',
|
|
|
|
|
|
fillOpacity: 1,
|
|
|
|
|
|
strokeColor: '#000',
|
|
|
|
|
|
strokeWeight: 1,
|
|
|
|
|
|
scale: 1.5,
|
|
|
|
|
|
anchor: new google.maps.Point(12, 12)
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
if (marker) unitMarkers.value.set(unit.user_id, marker);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Failed to update active units', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sonarHtml = `
|
|
|
|
|
|
<div style="position: relative; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center;">
|
|
|
|
|
|
<div style="position: absolute; width: 100%; height: 100%; border-radius: 50%; background-color: rgba(0, 212, 255, 0.5); animation: sonar-pulse 2.5s infinite ease-out;"></div>
|
|
|
|
|
|
<div style="position: absolute; width: 100%; height: 100%; border-radius: 50%; background-color: rgba(0, 212, 255, 0.3); animation: sonar-pulse 2.5s infinite ease-out; animation-delay: 1.25s;"></div>
|
|
|
|
|
|
<div style="width: 16px; height: 16px; background-color: #00d4ff; border-radius: 50%; box-shadow: 0 0 20px #00d4ff, 0 0 40px rgba(0, 212, 255, 0.6); border: 2px solid white; z-index: 2;"></div>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
@keyframes sonar-pulse {
|
|
|
|
|
|
0% { transform: scale(0.1); opacity: 0.8; }
|
|
|
|
|
|
100% { transform: scale(4); opacity: 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const optimalSonarHtml = `
|
|
|
|
|
|
<div style="position: relative; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center;">
|
|
|
|
|
|
<div style="position: absolute; width: 100%; height: 100%; border-radius: 50%; background-color: rgba(255, 165, 0, 0.5); animation: sonar-pulse 2.5s infinite ease-out;"></div>
|
|
|
|
|
|
<div style="position: absolute; width: 100%; height: 100%; border-radius: 50%; background-color: rgba(255, 165, 0, 0.3); animation: sonar-pulse 2.5s infinite ease-out; animation-delay: 1.25s;"></div>
|
|
|
|
|
|
<div style="width: 16px; height: 16px; background-color: #FFA500; border-radius: 50%; box-shadow: 0 0 20px #FFA500, 0 0 40px rgba(255, 165, 0, 0.6); border: 2px solid white; z-index: 2;"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
function locateUser() {
|
|
|
|
|
|
if (navigator.geolocation) {
|
|
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
|
|
|
|
(position) => {
|
|
|
|
|
|
const { latitude, longitude } = position.coords;
|
|
|
|
|
|
setCenter(latitude, longitude);
|
|
|
|
|
|
setZoom(16);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove existing user marker/sonar if any
|
|
|
|
|
|
if (userMarker.value) {
|
|
|
|
|
|
if (typeof userMarker.value.setMap === 'function') {
|
|
|
|
|
|
userMarker.value.setMap(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Guardamos la ubicación para navegaciones futuras
|
|
|
|
|
|
userCoords.value = { lat: latitude, lng: longitude };
|
|
|
|
|
|
|
|
|
|
|
|
// Add the CELESTE SONAR using HTML Marker
|
|
|
|
|
|
// Offset is negative half of the container size (60px/2 = 30)
|
|
|
|
|
|
userMarker.value = addHtmlMarker(
|
|
|
|
|
|
{ lat: latitude, lng: longitude },
|
|
|
|
|
|
sonarHtml,
|
|
|
|
|
|
{ x: -30, y: -30 }
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
(error) => {
|
|
|
|
|
|
console.error("Error getting location", error);
|
|
|
|
|
|
alert("No se pudo obtener tu ubicación. Por favor, verifica tus permisos de GPS.");
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* CÁLCULO DE INTELIGENCIA VIAL:
|
|
|
|
|
|
* Encuentra la parada más cercana dentro de la ruta seleccionada
|
|
|
|
|
|
* y la resalta para el usuario.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function highlightOptimalStopForRoute() {
|
|
|
|
|
|
if (!userCoords.value || routeStore.selectedRouteStops.length === 0) {
|
|
|
|
|
|
console.warn('🤖 JARVIS: Sin ubicación o paradas para calcular parada óptima.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('🤖 JARVIS: Calculando punto de abordaje óptimo sobre la ruta...');
|
|
|
|
|
|
|
|
|
|
|
|
let nearestStop = null;
|
|
|
|
|
|
let minDistance = Infinity;
|
|
|
|
|
|
|
|
|
|
|
|
const getDistance = (l1: any, l2: any) => {
|
|
|
|
|
|
const R = 6371; // Radio de la Tierra en km
|
|
|
|
|
|
const dLat = (l2.lat - l1.lat) * Math.PI / 180;
|
|
|
|
|
|
const dLon = (l2.lng - l1.lng) * Math.PI / 180;
|
|
|
|
|
|
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
|
|
|
|
|
Math.cos(l1.lat * Math.PI / 180) * Math.cos(l2.lat * Math.PI / 180) *
|
|
|
|
|
|
Math.sin(dLon/2) * Math.sin(dLon/2);
|
|
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
|
|
|
|
return R * c;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
routeStore.selectedRouteStops.forEach(stop => {
|
|
|
|
|
|
const dist = getDistance(userCoords.value, { lat: stop.latitude, lng: stop.longitude });
|
|
|
|
|
|
if (dist < minDistance) {
|
|
|
|
|
|
minDistance = dist;
|
|
|
|
|
|
nearestStop = stop;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (nearestStop) {
|
|
|
|
|
|
const stopObj = nearestStop as BusStop;
|
|
|
|
|
|
console.log(`🤖 JARVIS: Parada óptima detectada: ${stopObj.name} (${minDistance.toFixed(2)} km)`);
|
|
|
|
|
|
|
|
|
|
|
|
// Centrar mapa en la parada para guiar al usuario
|
|
|
|
|
|
setCenter(stopObj.latitude, stopObj.longitude);
|
|
|
|
|
|
setZoom(17);
|
|
|
|
|
|
|
|
|
|
|
|
// Añadir el PULSO NARANJA de "Aborda aquí"
|
|
|
|
|
|
if (optimalStopPulse.value && typeof optimalStopPulse.value.setMap === 'function') {
|
|
|
|
|
|
optimalStopPulse.value.setMap(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
optimalStopPulse.value = addHtmlMarker(
|
|
|
|
|
|
{ lat: stopObj.latitude, lng: stopObj.longitude },
|
|
|
|
|
|
optimalSonarHtml,
|
|
|
|
|
|
{ x: -30, y: -30 }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Mini-notificación informativa
|
|
|
|
|
|
navigationInfo.value = {
|
|
|
|
|
|
distance: minDistance < 1 ? `${(minDistance * 1000).toFixed(0)} m` : `${minDistance.toFixed(1)} km`,
|
|
|
|
|
|
duration: "Más cercana",
|
|
|
|
|
|
targetName: stopObj.name
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Dibuja la ruta caminando en el mapa interno sin abrir apps externas
|
|
|
|
|
|
*/
|
|
|
|
|
|
function drawInternalWalkingRoute(targetStop: BusStop, originOverride?: { lat: number, lng: number }) {
|
|
|
|
|
|
const origin = originOverride || userCoords.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (!origin) {
|
|
|
|
|
|
// Si no tenemos ubicación, la pedimos
|
|
|
|
|
|
navigator.geolocation.getCurrentPosition((pos) => {
|
|
|
|
|
|
userCoords.value = { lat: pos.coords.latitude, lng: pos.coords.longitude };
|
|
|
|
|
|
calculateWalkingPath(userCoords.value, targetStop);
|
|
|
|
|
|
}, (_err) => {
|
|
|
|
|
|
alert("Necesitamos tu ubicación para trazar la ruta a pie.");
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
calculateWalkingPath(origin, targetStop);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calculateWalkingPath(origin: { lat: number, lng: number }, targetStop: BusStop) {
|
|
|
|
|
|
// 1. Limpiar pulso anterior si existe
|
|
|
|
|
|
if (optimalStopPulse.value) {
|
|
|
|
|
|
if (typeof optimalStopPulse.value.setMap === 'function') {
|
|
|
|
|
|
optimalStopPulse.value.setMap(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
optimalStopPulse.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Añadir el PULSO NARANJA de "Parada Óptima"
|
|
|
|
|
|
optimalStopPulse.value = addHtmlMarker(
|
|
|
|
|
|
{ lat: targetStop.latitude, lng: targetStop.longitude },
|
|
|
|
|
|
optimalSonarHtml,
|
|
|
|
|
|
{ x: -30, y: -30 }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Resaltar la parada en verde (marcador estándar)
|
|
|
|
|
|
const targetStops = routeStore.selectedRouteId ? routeStore.selectedRouteStops : busStopStore.busStops;
|
|
|
|
|
|
const stopIndex = targetStops.findIndex(s => s.id === targetStop.id);
|
|
|
|
|
|
|
|
|
|
|
|
if (stopIndex !== -1 && markers.value[stopIndex]) {
|
|
|
|
|
|
const marker = markers.value[stopIndex];
|
|
|
|
|
|
marker.setIcon({
|
|
|
|
|
|
path: currentMarkerMode.value === 'pin'
|
|
|
|
|
|
? "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"
|
|
|
|
|
|
: google.maps.SymbolPath.CIRCLE,
|
|
|
|
|
|
fillColor: '#4CAF50',
|
|
|
|
|
|
fillOpacity: 1,
|
|
|
|
|
|
strokeColor: '#FFFFFF',
|
|
|
|
|
|
strokeWeight: 2,
|
|
|
|
|
|
scale: currentMarkerMode.value === 'pin' ? 2.5 : 9,
|
|
|
|
|
|
anchor: currentMarkerMode.value === 'pin' ? new google.maps.Point(12, 22) : null
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Trazar línea de puntos verde siguiendo RED VIAL PRINCIPAL
|
|
|
|
|
|
const directionsService = new google.maps.DirectionsService();
|
|
|
|
|
|
directionsService.route({
|
|
|
|
|
|
origin: origin,
|
|
|
|
|
|
destination: { lat: targetStop.latitude, lng: targetStop.longitude },
|
|
|
|
|
|
travelMode: google.maps.TravelMode.DRIVING,
|
|
|
|
|
|
}, (dirResult, dirStatus) => {
|
|
|
|
|
|
if (dirStatus === 'OK' && dirResult && dirResult.routes && dirResult.routes[0]) {
|
|
|
|
|
|
const route = dirResult.routes[0];
|
|
|
|
|
|
const leg = route.legs?.[0];
|
|
|
|
|
|
|
|
|
|
|
|
// Guardar info de navegación (ETA y Distancia)
|
|
|
|
|
|
if (leg) {
|
|
|
|
|
|
navigationInfo.value = {
|
|
|
|
|
|
distance: leg.distance?.text || '---',
|
|
|
|
|
|
duration: leg.duration?.text || '---',
|
|
|
|
|
|
targetName: targetStop.name
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (walkingPolyline.value) walkingPolyline.value.setMap(null);
|
|
|
|
|
|
if (walkingPolylineBorder.value) walkingPolylineBorder.value.setMap(null);
|
|
|
|
|
|
|
|
|
|
|
|
// CAPA 1: Borde blanco (Para dar contraste estilo Google Maps)
|
|
|
|
|
|
walkingPolylineBorder.value = new google.maps.Polyline({
|
|
|
|
|
|
path: route.overview_path,
|
|
|
|
|
|
geodesic: true,
|
|
|
|
|
|
strokeColor: '#FFFFFF',
|
|
|
|
|
|
strokeOpacity: 0.9,
|
|
|
|
|
|
strokeWeight: 10, // Un poco más grueso para el borde
|
|
|
|
|
|
map: map.value,
|
|
|
|
|
|
zIndex: 5
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// CAPA 2: Línea Indigo Central (La ruta principal)
|
|
|
|
|
|
walkingPolyline.value = new google.maps.Polyline({
|
|
|
|
|
|
path: route.overview_path,
|
|
|
|
|
|
geodesic: true,
|
|
|
|
|
|
strokeColor: '#4285F4', // Azul Google Maps
|
|
|
|
|
|
strokeOpacity: 1.0,
|
|
|
|
|
|
strokeWeight: 5,
|
|
|
|
|
|
map: map.value,
|
|
|
|
|
|
zIndex: 10
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Ajustar zoom para mostrar toda la ruta de caminata
|
|
|
|
|
|
if (map.value) {
|
|
|
|
|
|
const bounds = new google.maps.LatLngBounds();
|
|
|
|
|
|
route.overview_path.forEach(p => bounds.extend(p));
|
|
|
|
|
|
map.value.fitBounds(bounds, { top: 100, bottom: 200, left: 50, right: 50 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearNavigation() {
|
|
|
|
|
|
clearMapMarkers();
|
|
|
|
|
|
navigationInfo.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="split-view">
|
|
|
|
|
|
<!-- Main Map Container -->
|
|
|
|
|
|
<div class="map-side">
|
|
|
|
|
|
<div class="map-view">
|
|
|
|
|
|
<div class="map-container">
|
|
|
|
|
|
<div v-if="mapsError" class="error">
|
|
|
|
|
|
<div style="text-align: center; padding: 20px; max-width: 600px; margin: 0 auto;">
|
|
|
|
|
|
<h3 style="color: var(--text-primary); margin-bottom: 15px;">⚠️ Error al cargar mapa</h3>
|
|
|
|
|
|
<div style="color: var(--text-primary); margin-bottom: 15px; white-space: pre-line; text-align: left; background: var(--bg-secondary); padding: 15px; border-radius: 8px;">
|
|
|
|
|
|
{{ mapsError }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="!isLoaded" class="loading">
|
|
|
|
|
|
<p>{{ t('map.loadingMap') }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Always render map div so it exists in DOM -->
|
|
|
|
|
|
<div id="map" class="map" :style="{ display: isLoaded ? 'block' : 'none' }"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Floating UI Elements -->
|
|
|
|
|
|
<div class="map-floating-controls">
|
|
|
|
|
|
<!-- Promos Toggle Button (High-Tech Animation) -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="isLoaded && couponStore.coupons.length > 0"
|
|
|
|
|
|
class="promos-badge-wrapper"
|
|
|
|
|
|
@click="showPromos = !showPromos"
|
|
|
|
|
|
:class="{ 'active': showPromos }"
|
|
|
|
|
|
title="Ver Ofertas"
|
|
|
|
|
|
>
|
|
|
|
|
|
<OffersBadge :is-close="showPromos" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Location Button (Animated Pin) -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="isLoaded"
|
|
|
|
|
|
class="location-loader-btn"
|
|
|
|
|
|
@click="locateUser"
|
|
|
|
|
|
:title="t('map.showMyLocation')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="material-icons">my_location</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Uber-like Search Interface -->
|
|
|
|
|
|
<div class="uber-search-container" :class="{ 'compact-mode': routeStore.selectedRouteId && !showUberSearch }">
|
|
|
|
|
|
<!-- Floating Triggers -->
|
|
|
|
|
|
<div v-if="!showUberSearch" class="triggers-row">
|
|
|
|
|
|
<!-- Shrunk Trigger (Icon only) -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="routeStore.selectedRouteId"
|
|
|
|
|
|
class="uber-search-trigger circular"
|
|
|
|
|
|
@click="openUberSearch"
|
|
|
|
|
|
title="Buscar"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="material-icons">search</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Normal Trigger -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="uber-search-trigger"
|
|
|
|
|
|
@click="openUberSearch"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="material-icons search-icon">search</span>
|
|
|
|
|
|
<span class="trigger-text">¿A dónde vamos?</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Google Maps Style Navigation Summary Card -->
|
|
|
|
|
|
<Transition name="uber-slide">
|
|
|
|
|
|
<div v-if="navigationInfo" class="navigation-summary-card">
|
|
|
|
|
|
<div class="nav-card-accent"></div>
|
|
|
|
|
|
<div class="nav-content">
|
|
|
|
|
|
<div class="nav-left">
|
|
|
|
|
|
<div class="nav-stats">
|
|
|
|
|
|
<span class="nav-time">{{ navigationInfo.duration }}</span>
|
|
|
|
|
|
<span class="nav-dist">{{ navigationInfo.distance }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-destination">Parada: {{ navigationInfo.targetName }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-actions">
|
|
|
|
|
|
<button class="nav-btn-close" @click="clearNavigation">
|
|
|
|
|
|
<span class="material-icons">close</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
<!-- Uber-style Search Panel -->
|
|
|
|
|
|
<Transition name="uber-slide">
|
|
|
|
|
|
<div v-if="showUberSearch" class="uber-search-panel stark-hud-panel">
|
|
|
|
|
|
<div class="hud-header">
|
|
|
|
|
|
<button class="hud-close-btn" @click="closeUberSearch">
|
|
|
|
|
|
<span class="material-icons">arrow_back</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="hud-title">PLANIFICAR VIAJE</div>
|
|
|
|
|
|
</div>
|
2026-02-21 09:53:31 -05:00
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
<div class="hud-search-box">
|
|
|
|
|
|
<div class="hud-inputs">
|
|
|
|
|
|
<div class="hud-input-row">
|
|
|
|
|
|
<span class="material-icons hud-icon origin">my_location</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
v-model="originQuery"
|
|
|
|
|
|
class="hud-input"
|
|
|
|
|
|
readonly
|
|
|
|
|
|
>
|
2026-02-21 09:53:31 -05:00
|
|
|
|
</div>
|
2026-02-22 13:21:44 -05:00
|
|
|
|
<div class="hud-connector"></div>
|
|
|
|
|
|
<div class="hud-input-row">
|
|
|
|
|
|
<span class="material-icons hud-icon dest">location_on</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
v-model="destinationQuery"
|
|
|
|
|
|
class="hud-input highlight"
|
|
|
|
|
|
:placeholder="t('map.where_to')"
|
|
|
|
|
|
@focus="onInputFocus"
|
|
|
|
|
|
@blur="onInputBlur"
|
2026-02-21 09:53:31 -05:00
|
|
|
|
>
|
|
|
|
|
|
</div>
|
2026-02-22 13:21:44 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="hud-options">
|
|
|
|
|
|
<button class="hud-clear-btn" @click="clearAllMapData">
|
|
|
|
|
|
<span class="material-icons">layers_clear</span>
|
|
|
|
|
|
<span>LIMPIAR MAPA</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<label class="hud-toggle">
|
|
|
|
|
|
<input type="checkbox" v-model="showRoutesToggle">
|
|
|
|
|
|
<span class="hud-toggle-text">VER RUTAS</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Results -->
|
|
|
|
|
|
<div v-if="filteredSearchResults.length > 0" class="hud-results">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="stop in filteredSearchResults"
|
|
|
|
|
|
:key="stop.id"
|
|
|
|
|
|
class="hud-result-item"
|
|
|
|
|
|
@click="selectStopFromSearch(stop)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="hud-result-icon">
|
|
|
|
|
|
<span class="material-icons">directions_bus</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="hud-result-info">
|
|
|
|
|
|
<div class="hud-result-name">{{ stop.name }}</div>
|
|
|
|
|
|
<div class="hud-result-desc">Parada de Autobús</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="material-icons hud-go-icon">chevron_right</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
2026-02-21 09:53:31 -05:00
|
|
|
|
|
|
|
|
|
|
<!-- Routes List (Visible when toggle is on) -->
|
|
|
|
|
|
<div v-if="showRoutesToggle" class="uber-results routes-list">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="route in routeStore.allRoutes"
|
|
|
|
|
|
:key="route.id"
|
|
|
|
|
|
class="uber-result-item"
|
|
|
|
|
|
:class="{ 'selected-route': route.id === routeStore.selectedRouteId }"
|
|
|
|
|
|
@click="selectRouteAndClose(route.id, route.name)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="result-icon">
|
|
|
|
|
|
<span class="material-icons">route</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="result-info">
|
|
|
|
|
|
<div class="result-name">{{ route.name }}</div>
|
|
|
|
|
|
<div class="result-address">Ruta de transporte público</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span v-if="route.id === routeStore.selectedRouteId" class="material-icons check-icon">check_circle</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Search Results -->
|
|
|
|
|
|
<div v-if="filteredSearchResults.length > 0 && !showRoutesToggle" class="uber-results">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="stop in filteredSearchResults"
|
|
|
|
|
|
:key="stop.id"
|
|
|
|
|
|
class="uber-result-item"
|
|
|
|
|
|
@click="selectStopFromSearch(stop)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="result-icon">
|
|
|
|
|
|
<span class="material-icons">history</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="result-info">
|
|
|
|
|
|
<div class="result-name">{{ stop.name }}</div>
|
|
|
|
|
|
<div class="result-address">Parada de bus cercana</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Floating Promos Carousel -->
|
|
|
|
|
|
<Transition name="promo-slide">
|
|
|
|
|
|
<div v-if="showPromos && couponStore.coupons.length > 0" class="floating-promos-container">
|
|
|
|
|
|
<div class="carousel-container glass-morphism">
|
|
|
|
|
|
<div class="carousel-header">
|
|
|
|
|
|
<span class="material-icons">stars</span>
|
|
|
|
|
|
<h2>Ofertas SIBU</h2>
|
|
|
|
|
|
<button class="close-promos-btn" @click="showPromos = false">
|
|
|
|
|
|
<span class="material-icons">close</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="carousel-viewport">
|
|
|
|
|
|
<div class="carousel-track">
|
|
|
|
|
|
<Transition name="carousel-slide" mode="out-in">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentPromo"
|
|
|
|
|
|
:key="currentPromo.id"
|
|
|
|
|
|
class="promo-card-premium"
|
|
|
|
|
|
@mouseenter="stopCarousel"
|
|
|
|
|
|
@mouseleave="startCarousel"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="promo-image-wrapper">
|
|
|
|
|
|
<img :src="getImageUrl(currentPromo.image_url)" class="promo-img-full" />
|
|
|
|
|
|
<div class="promo-brand-badge">
|
|
|
|
|
|
{{ currentPromo.business_name || 'SIBU' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="currentPromo.discount_percentage" class="promo-discount-tag">
|
|
|
|
|
|
-{{ currentPromo.discount_percentage }}%
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="promo-info-premium">
|
|
|
|
|
|
<h3 class="promo-title-premium">{{ currentPromo.title }}</h3>
|
|
|
|
|
|
<p class="promo-desc-premium">{{ currentPromo.description }}</p>
|
|
|
|
|
|
<button class="promo-btn-premium" @click="router.push('/business/' + currentPromo.business_id)">
|
|
|
|
|
|
Ver Negocio
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Carousel Controls -->
|
|
|
|
|
|
<div class="carousel-dots">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="(_, index) in couponStore.coupons"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="dot"
|
|
|
|
|
|
:class="{ active: index === currentCarouselIndex }"
|
|
|
|
|
|
@click="currentCarouselIndex = index"
|
|
|
|
|
|
></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button class="carousel-nav prev" @click="prevPromo">
|
|
|
|
|
|
<span class="material-icons">chevron_left</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="carousel-nav next" @click="nextPromo">
|
|
|
|
|
|
<span class="material-icons">chevron_right</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Modal for details -->
|
|
|
|
|
|
<BusStopInfoModal
|
|
|
|
|
|
:is-open="showBusStopModal"
|
|
|
|
|
|
:bus-stop="selectedBusStop"
|
|
|
|
|
|
@close="closeBusStopModal"
|
|
|
|
|
|
@navigate="drawInternalWalkingRoute"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<Transition name="modal-fade">
|
|
|
|
|
|
<div v-if="showPromoModal && selectedPromo" class="promo-modal-overlay" @click="closePromoModal">
|
|
|
|
|
|
<div class="promo-modal-content" @click.stop>
|
|
|
|
|
|
<div class="promo-header-modal">
|
|
|
|
|
|
<img :src="getImageUrl(selectedPromo.image_url)" class="promo-img-modal" />
|
|
|
|
|
|
<div class="promo-badge-modal">PROMO</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="promo-body-modal">
|
|
|
|
|
|
<h2 class="promo-title-modal">{{ selectedPromo.title }}</h2>
|
|
|
|
|
|
<div class="promo-biz">{{ selectedPromo.business_name }}</div>
|
|
|
|
|
|
<p>{{ selectedPromo.description }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="promo-actions-modal">
|
|
|
|
|
|
<button class="business-detail-btn-modal" @click="router.push('/business/' + selectedPromo.business_id)">Ver Negocio</button>
|
|
|
|
|
|
<button class="promo-claim-btn" @click="claimPromo">Reclamar Cupón</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.split-view {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: calc(100vh - 64px); /* Adjust based on header height */
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map-side {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map-view {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.map {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Premium Carousel Styles */
|
|
|
|
|
|
.carousel-container {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
border-radius: 28px;
|
|
|
|
|
|
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.glass-morphism {
|
|
|
|
|
|
background: var(--card-bg);
|
|
|
|
|
|
backdrop-filter: blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: blur(20px);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding-left: 48px; /* Space for the absolute close button */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-header h2 {
|
|
|
|
|
|
font-size: 1.6rem; /* Slightly smaller for better fit */
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-promos-btn {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-promos-btn:hover {
|
|
|
|
|
|
background: var(--hover-bg);
|
|
|
|
|
|
color: #f44336;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-viewport {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-track {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 440px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-card-premium {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%; /* Fill the carousel viewport */
|
|
|
|
|
|
background: var(--card-bg);
|
|
|
|
|
|
border-radius: 24px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-image-wrapper {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 180px; /* Reduced from 280px to save space */
|
|
|
|
|
|
min-height: 180px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-img-full {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-brand-badge {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 12px;
|
|
|
|
|
|
left: 12px;
|
|
|
|
|
|
background: rgba(255,255,255,0.9);
|
|
|
|
|
|
color: #101820;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
backdrop-filter: blur(5px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-discount-tag {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: -15px;
|
|
|
|
|
|
right: 16px;
|
|
|
|
|
|
background: #E91E63;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 6px 14px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
|
box-shadow: 0 5px 15px rgba(233, 30, 99, 0.4);
|
|
|
|
|
|
z-index: 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-info-premium {
|
|
|
|
|
|
padding: 24px 20px 20px; /* Reduced padding */
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-title-premium {
|
|
|
|
|
|
font-size: 1.2rem; /* Reduced from 1.5rem */
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 1;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
line-clamp: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-desc-premium {
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 3; /* Limit description to 3 lines */
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
line-clamp: 3;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
flex: 1; /* Push the button to the bottom */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-btn-premium {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
background: var(--text-primary);
|
|
|
|
|
|
color: var(--bg-primary);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
margin-top: auto; /* Ensure button stays at bottom */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-btn-premium:hover {
|
|
|
|
|
|
transform: translateY(-3px);
|
|
|
|
|
|
background: var(--active-color);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Dots and Nav */
|
|
|
|
|
|
.carousel-dots {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--border-color);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dot.active {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: var(--active-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-nav {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
background: rgba(255,255,255,0.8);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
|
|
|
|
|
z-index: 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-nav.prev { left: -25px; }
|
|
|
|
|
|
.carousel-nav.next { right: -25px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Carousel Slide Animation */
|
|
|
|
|
|
.carousel-slide-enter-active,
|
|
|
|
|
|
.carousel-slide-leave-active {
|
|
|
|
|
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-slide-enter-from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(100px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-slide-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateX(-100px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Uber-like Search Interface Styles */
|
|
|
|
|
|
.uber-search-container {
|
2026-02-21 17:43:46 -05:00
|
|
|
|
position: fixed;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
top: 90px;
|
|
|
|
|
|
left: 16px;
|
|
|
|
|
|
right: 16px;
|
2026-02-21 17:43:46 -05:00
|
|
|
|
z-index: 1100;
|
|
|
|
|
|
pointer-events: none;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-search-container > * {
|
|
|
|
|
|
pointer-events: auto; /* Re-enable for children */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-search-trigger {
|
|
|
|
|
|
background: var(--header-bg);
|
|
|
|
|
|
backdrop-filter: blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: blur(20px);
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
width: 100%; /* Take full width of container */
|
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-search-trigger.circular {
|
|
|
|
|
|
width: 60px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.triggers-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.schedules-btn-floating {
|
|
|
|
|
|
background: linear-gradient(135deg, #fee715 0%, #facc15 100%);
|
|
|
|
|
|
color: #101820;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
padding: 0 24px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
box-shadow: 0 10px 20px rgba(254, 231, 21, 0.2);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.schedules-btn-floating:hover {
|
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
|
box-shadow: 0 15px 30px rgba(254, 231, 21, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-search-trigger:hover {
|
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
|
background: var(--hover-bg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-icon {
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.trigger-text {
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
letter-spacing: -0.01em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.stark-hud-panel {
|
2026-02-21 17:43:46 -05:00
|
|
|
|
position: fixed;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
top: 15px;
|
|
|
|
|
|
left: 15px;
|
|
|
|
|
|
right: 15px;
|
|
|
|
|
|
background: #0f172a; /* Solid Deep Navy */
|
2026-02-21 09:53:31 -05:00
|
|
|
|
border: 1px solid var(--border-color);
|
2026-02-22 13:21:44 -05:00
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
z-index: 2500;
|
|
|
|
|
|
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
|
|
|
|
|
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Elevación Stark al enfocar teclado */
|
|
|
|
|
|
.stark-hud-panel.elevated,
|
|
|
|
|
|
.stark-hud-panel:focus-within {
|
|
|
|
|
|
transform: translateY(-5px);
|
|
|
|
|
|
border-color: var(--active-color);
|
|
|
|
|
|
box-shadow: 0 10px 30px rgba(254, 231, 21, 0.15);
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-header {
|
2026-02-21 09:53:31 -05:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
gap: 12px;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-close-btn {
|
2026-02-21 09:53:31 -05:00
|
|
|
|
background: var(--hover-bg);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--text-primary);
|
2026-02-22 13:21:44 -05:00
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 10px;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
cursor: pointer;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-title {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
letter-spacing: 1.5px;
|
|
|
|
|
|
color: var(--active-color);
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-search-box {
|
2026-02-21 09:53:31 -05:00
|
|
|
|
background: var(--bg-secondary);
|
2026-02-22 13:21:44 -05:00
|
|
|
|
border-radius: 15px;
|
|
|
|
|
|
padding: 12px;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-inputs {
|
2026-02-21 09:53:31 -05:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-input-row {
|
|
|
|
|
|
display: flex;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
align-items: center;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
gap: 12px;
|
|
|
|
|
|
background: rgba(255,255,255,0.03);
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
transition: all 0.3s;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-input-row:focus-within {
|
|
|
|
|
|
background: rgba(254, 231, 21, 0.05);
|
|
|
|
|
|
border-color: var(--active-color);
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-icon {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-icon.origin { color: #3b82f6; } /* Blue for origin */
|
|
|
|
|
|
.hud-icon.dest { color: var(--active-color); } /* Yellow for destination */
|
|
|
|
|
|
|
|
|
|
|
|
.hud-input {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-connector {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 20px;
|
|
|
|
|
|
top: 35px;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
width: 2px;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
height: 15px;
|
|
|
|
|
|
background: var(--border-color);
|
|
|
|
|
|
z-index: 1;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-options {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding-top: 5px;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-clear-btn {
|
|
|
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 800;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
display: flex;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
cursor: pointer;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-toggle {
|
2026-02-21 09:53:31 -05:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
cursor: pointer;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 13:21:44 -05:00
|
|
|
|
.hud-toggle input {
|
|
|
|
|
|
accent-color: var(--active-color);
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-toggle-text {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-results {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
max-height: 40vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-result-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
2026-02-21 09:53:31 -05:00
|
|
|
|
border-radius: 12px;
|
2026-02-22 13:21:44 -05:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-result-item:hover {
|
|
|
|
|
|
background: var(--hover-bg);
|
|
|
|
|
|
border-color: var(--border-color);
|
|
|
|
|
|
transform: translateX(4px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-result-icon {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
background: rgba(254, 231, 21, 0.1);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-result-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-result-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 700;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
color: var(--text-primary);
|
2026-02-22 13:21:44 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-result-desc {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hud-go-icon {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
opacity: 0.5;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-input:focus {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.07);
|
|
|
|
|
|
border-color: var(--active-color);
|
|
|
|
|
|
box-shadow: 0 0 0 4px rgba(254, 231, 21, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-options {
|
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
|
border-top: 1px solid var(--border-color);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clear-map-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clear-map-btn:hover {
|
|
|
|
|
|
background: rgba(239, 68, 68, 0.2);
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clear-map-btn:active {
|
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clear-map-btn .material-icons {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-toggle {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-toggle input {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
accent-color: var(--active-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-results {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
max-height: 55vh; /* Ajustado para dar espacio a la barra inferior */
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding-bottom: 120px; /* Suficiente espacio para que no lo tape la barra de navegación */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-result-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-result-item:hover {
|
|
|
|
|
|
background: var(--hover-bg);
|
|
|
|
|
|
transform: translateX(8px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selected-route {
|
|
|
|
|
|
background: var(--active-bg);
|
|
|
|
|
|
border: 1px solid var(--active-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.check-icon {
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-icon {
|
|
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-name {
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-address {
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Uber Slide Animation */
|
|
|
|
|
|
.uber-slide-enter-active,
|
|
|
|
|
|
.uber-slide-leave-active {
|
|
|
|
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.uber-slide-enter-from,
|
|
|
|
|
|
.uber-slide-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(20px) scale(0.95);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Reposicion de elementos fijos */
|
|
|
|
|
|
.map-floating-controls {
|
2026-02-21 17:43:46 -05:00
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 85px;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
right: 16px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
z-index: 1100;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promos-badge-wrapper {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promos-badge-wrapper:hover {
|
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-promos-icon {
|
|
|
|
|
|
width: 50px;
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: #ef4444;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.location-loader-btn {
|
|
|
|
|
|
background: var(--header-bg);
|
|
|
|
|
|
backdrop-filter: blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: blur(20px);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 50px;
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
box-shadow: var(--shadow);
|
|
|
|
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.location-loader-btn:hover {
|
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
|
background: var(--hover-bg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.location-loader-btn .material-icons {
|
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.promos-toggle-btn {
|
|
|
|
|
|
width: 60px;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
background: var(--active-color);
|
|
|
|
|
|
color: #101820;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
box-shadow: 0 8px 25px rgba(254, 231, 21, 0.4);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promos-toggle-btn.active {
|
|
|
|
|
|
background: var(--text-primary);
|
|
|
|
|
|
color: var(--bg-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promos-toggle-btn .material-icons {
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.notification-dot {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -4px;
|
|
|
|
|
|
right: -4px;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
background: #f44336;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 2px solid var(--bg-primary);
|
|
|
|
|
|
animation: pulse-dot 2s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse-dot {
|
|
|
|
|
|
0% { transform: scale(1); }
|
|
|
|
|
|
50% { transform: scale(1.2); }
|
|
|
|
|
|
100% { transform: scale(1); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pulse {
|
|
|
|
|
|
animation: pulse-animation 2s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse-animation {
|
|
|
|
|
|
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(254, 231, 21, 0.7); }
|
|
|
|
|
|
70% { transform: scale(1.1); box-shadow: 0 0 0 15px rgba(254, 231, 21, 0); }
|
|
|
|
|
|
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(254, 231, 21, 0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.floating-promos-container {
|
2026-02-21 17:43:46 -05:00
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 50%;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 400px;
|
|
|
|
|
|
max-width: calc(100vw - 40px);
|
2026-02-21 17:43:46 -05:00
|
|
|
|
z-index: 1300;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Transitions */
|
|
|
|
|
|
.promo-slide-enter-active,
|
|
|
|
|
|
.promo-slide-leave-active {
|
|
|
|
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-slide-enter-from,
|
|
|
|
|
|
.promo-slide-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(20px) scale(0.95);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.location-button .material-icons { font-size: 24px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Responsive */
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.uber-search-container {
|
|
|
|
|
|
top: 80px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.floating-promos-container {
|
|
|
|
|
|
width: 92%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Reposition floating controls for mobile thumb reach */
|
|
|
|
|
|
.map-floating-controls {
|
2026-02-21 17:43:46 -05:00
|
|
|
|
bottom: 120px; /* Más espacio para pulgares en móvil */
|
|
|
|
|
|
right: 15px;
|
2026-02-21 09:53:31 -05:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
z-index: 1100;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Adjust carousel height for smaller screens if needed */
|
|
|
|
|
|
/* Adjust carousel size for mobile */
|
|
|
|
|
|
.carousel-track {
|
|
|
|
|
|
height: 350px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-image-wrapper {
|
|
|
|
|
|
height: 120px;
|
|
|
|
|
|
min-height: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-header h2 {
|
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-title-premium {
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-desc-premium {
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
|
line-clamp: 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.promo-info-premium {
|
|
|
|
|
|
padding: 16px 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Modal Simple Styles (already mostly covered) */
|
|
|
|
|
|
.promo-modal-overlay {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0; left: 0; width: 100%; height: 100%;
|
|
|
|
|
|
background: rgba(0,0,0,0.8);
|
|
|
|
|
|
z-index: 3000;
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.promo-modal-content {
|
|
|
|
|
|
background: var(--card-bg); width: 90%; max-width: 450px;
|
|
|
|
|
|
border-radius: 20px; overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.promo-header-modal { position: relative; height: 200px; }
|
|
|
|
|
|
.promo-img-modal { width: 100%; height: 100%; object-fit: cover; }
|
|
|
|
|
|
.promo-badge-modal { position: absolute; bottom: 0; left: 0; background: #FF4081; color: white; padding: 5px 15px; font-weight: 800; }
|
|
|
|
|
|
.promo-body-modal { padding: 25px; }
|
|
|
|
|
|
.promo-title-modal { font-size: 1.5rem; font-weight: 800; margin-bottom: 10px; }
|
|
|
|
|
|
.promo-biz { color: var(--active-color); font-weight: 700; margin-bottom: 15px; }
|
|
|
|
|
|
.promo-actions-modal { padding: 0 25px 25px; display: flex; gap: 10px; }
|
|
|
|
|
|
.promo-claim-btn { flex: 1; background: #FF4081; color: white; border: none; padding: 15px; border-radius: 10px; font-weight: 700; cursor: pointer; }
|
|
|
|
|
|
.business-detail-btn-modal { flex: 1; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); padding: 15px; border-radius: 10px; font-weight: 700; cursor: pointer; }
|
|
|
|
|
|
.close-modal-btn {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 15px;
|
|
|
|
|
|
right: 15px;
|
|
|
|
|
|
background: rgba(0,0,0,0.5);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-modal-btn:hover {
|
|
|
|
|
|
background: rgba(0,0,0,0.8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tourist-badge {
|
|
|
|
|
|
background: #4CAF50 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.business-category-chip {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border-radius: 100px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.business-detail-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.business-detail-item .material-icons {
|
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
|
color: var(--active-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.call-btn {
|
|
|
|
|
|
background: #1976D2 !important;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Google Maps Style Navigation Card */
|
|
|
|
|
|
.navigation-summary-card {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0px;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
margin: 12px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
box-shadow: 0 4px 25px rgba(0,0,0,0.2);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-card-accent {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
background: #4285F4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-left {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-stats {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-time {
|
|
|
|
|
|
font-size: 1.4rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #1a73e8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-dist {
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
color: #5f6368;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-destination {
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
color: #202124;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
max-width: 250px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-btn-close {
|
|
|
|
|
|
background: #f1f3f4;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: #5f6368;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-btn-close:hover {
|
|
|
|
|
|
background: #e8eaed;
|
|
|
|
|
|
color: #202124;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
|
|
.navigation-summary-card {
|
|
|
|
|
|
background: #202124;
|
|
|
|
|
|
border-color: rgba(255,255,255,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-time { color: #8ab4f8; }
|
|
|
|
|
|
.nav-dist { color: #bdc1c6; }
|
|
|
|
|
|
.nav-destination { color: #e8eaed; }
|
|
|
|
|
|
.nav-btn-close { background: #3c4043; color: #bdc1c6; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|