From fd95df461b40c06fa2d5df447df5ffe98a5e9932 Mon Sep 17 00:00:00 2001 From: Hanzo_dev <2002samudiojohan@gmail.com> Date: Wed, 25 Feb 2026 16:29:13 -0500 Subject: [PATCH] fix: critical bug fixes - routes UUID, image paths, favorites loading, bottom nav debounce --- backend/app/api/businesses.py | 31 +++------- backend/app/api/coupons.py | 75 +++++++++++++++++++---- backend/app/api/routes/__init__.py | 85 +++++++++++++++++++++++---- backend/app/api/shuttles/__init__.py | 26 ++++---- backend/app/api/taxis/__init__.py | 25 ++++---- backend/app/main.py | 21 +++++-- backend/app/services/image_handler.py | 79 +++++++++++++++++++++++++ frontend/src/App.vue | 8 +++ frontend/src/components/BottomNav.vue | 12 +++- frontend/src/utils/imageUrl.ts | 38 ++++++++++++ frontend/src/views/AdminShuttles.vue | 10 ++-- frontend/src/views/DiscoverView.vue | 28 ++++++--- frontend/src/views/MapView.vue | 20 ++++--- frontend/src/views/TaxiView.vue | 37 ++++++++---- 14 files changed, 379 insertions(+), 116 deletions(-) create mode 100644 backend/app/services/image_handler.py create mode 100644 frontend/src/utils/imageUrl.ts diff --git a/backend/app/api/businesses.py b/backend/app/api/businesses.py index 4e05e13..92b42fb 100644 --- a/backend/app/api/businesses.py +++ b/backend/app/api/businesses.py @@ -6,6 +6,8 @@ from app.models.business import Business from app.models.user import User, UserRole from app.api.deps import get_current_user +from app.services.image_handler import save_image, delete_image + router = APIRouter(prefix="/api/businesses", tags=["businesses"]) @router.get("", response_model=List[Business]) @@ -41,17 +43,7 @@ async def create_business( image_url = None if image: - import os - import shutil - from uuid import uuid4 - UPLOAD_DIR = "uploads/businesses" - os.makedirs(UPLOAD_DIR, exist_ok=True) - ext = os.path.splitext(image.filename)[1] - filename = f"{uuid4()}{ext}" - path = os.path.join(UPLOAD_DIR, filename) - with open(path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) - image_url = f"/uploads/businesses/{filename}" + image_url = save_image(image, "businesses") db_business = Business( name=name, @@ -110,17 +102,9 @@ async def update_business( db_business.longitude = longitude if image: - import os - import shutil - from uuid import uuid4 - UPLOAD_DIR = "uploads/businesses" - os.makedirs(UPLOAD_DIR, exist_ok=True) - ext = os.path.splitext(image.filename)[1] - filename = f"{uuid4()}{ext}" - path = os.path.join(UPLOAD_DIR, filename) - with open(path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) - db_business.image_url = f"/uploads/businesses/{filename}" + if db_business.image_url: + delete_image(db_business.image_url) + db_business.image_url = save_image(image, "businesses") session.add(db_business) session.commit() @@ -153,6 +137,9 @@ async def delete_business( if not db_business: raise HTTPException(status_code=404, detail="Business not found") + if db_business.image_url: + delete_image(db_business.image_url) + session.delete(db_business) session.commit() return {"status": "success", "message": "Business deleted"} diff --git a/backend/app/api/coupons.py b/backend/app/api/coupons.py index b9583f5..9d27844 100644 --- a/backend/app/api/coupons.py +++ b/backend/app/api/coupons.py @@ -1,11 +1,14 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Form, File, UploadFile from sqlmodel import Session, select from sqlalchemy.orm import joinedload -from typing import List +from typing import List, Optional +from uuid import UUID +from datetime import datetime from app.core.database import get_session -from app.models.coupon import Coupon, CouponCreate, CouponUpdate +from app.models.coupon import Coupon from app.models.user import User, UserRole from app.api.deps import get_current_user +from app.services.image_handler import save_image, delete_image router = APIRouter(prefix="/api/coupons", tags=["coupons"]) @@ -27,7 +30,19 @@ async def list_coupons( async def create_coupon( *, session: Session = Depends(get_session), - coupon_in: CouponCreate, + title: str = Form(...), + business_id: Optional[UUID] = Form(None), + description: Optional[str] = Form(None), + business_name: Optional[str] = Form(None), + business_address: Optional[str] = Form(None), + business_phone: Optional[str] = Form(None), + discount_percentage: Optional[int] = Form(None), + discount_amount: Optional[float] = Form(None), + category: Optional[str] = Form(None), + valid_from: Optional[datetime] = Form(None), + valid_until: Optional[datetime] = Form(None), + is_active: bool = Form(True), + image: Optional[UploadFile] = File(None), current_user: User = Depends(get_current_user) ): """Create a new coupon (Promoters and Admins only).""" @@ -37,7 +52,25 @@ async def create_coupon( detail="Only promoters and admins can create coupons" ) - db_coupon = Coupon.from_orm(coupon_in) + image_url = None + if image: + image_url = save_image(image, "coupons") + + db_coupon = Coupon( + title=title, + business_id=business_id, + description=description, + business_name=business_name, + business_address=business_address, + business_phone=business_phone, + discount_percentage=discount_percentage, + discount_amount=discount_amount, + category=category, + valid_from=valid_from, + valid_until=valid_until, + is_active=is_active, + image_url=image_url + ) session.add(db_coupon) session.commit() session.refresh(db_coupon) @@ -47,8 +80,13 @@ async def create_coupon( async def update_coupon( *, session: Session = Depends(get_session), - coupon_id: str, - coupon_in: CouponUpdate, + coupon_id: UUID, + title: Optional[str] = Form(None), + description: Optional[str] = Form(None), + discount_percentage: Optional[int] = Form(None), + valid_until: Optional[datetime] = Form(None), + is_active: Optional[bool] = Form(None), + image: Optional[UploadFile] = File(None), current_user: User = Depends(get_current_user) ): """Update a coupon (Promoters and Admins only).""" @@ -62,9 +100,21 @@ async def update_coupon( if not db_coupon: raise HTTPException(status_code=404, detail="Coupon not found") - coupon_data = coupon_in.dict(exclude_unset=True) - for key, value in coupon_data.items(): - setattr(db_coupon, key, value) + if title is not None: + db_coupon.title = title + if description is not None: + db_coupon.description = description + if discount_percentage is not None: + db_coupon.discount_percentage = discount_percentage + if valid_until is not None: + db_coupon.valid_until = valid_until + if is_active is not None: + db_coupon.is_active = is_active + + if image: + if db_coupon.image_url: + delete_image(db_coupon.image_url) + db_coupon.image_url = save_image(image, "coupons") session.add(db_coupon) session.commit() @@ -75,7 +125,7 @@ async def update_coupon( async def delete_coupon( *, session: Session = Depends(get_session), - coupon_id: str, + coupon_id: UUID, current_user: User = Depends(get_current_user) ): """Delete a coupon (Promoters and Admins only).""" @@ -89,6 +139,9 @@ async def delete_coupon( if not db_coupon: raise HTTPException(status_code=404, detail="Coupon not found") + if db_coupon.image_url: + delete_image(db_coupon.image_url) + session.delete(db_coupon) session.commit() return {"status": "success", "message": "Coupon deleted"} diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py index 0f2f21a..5fd6e8e 100644 --- a/backend/app/api/routes/__init__.py +++ b/backend/app/api/routes/__init__.py @@ -46,16 +46,24 @@ async def get_route_stops(route_id: str, session: Session = Depends(get_session) """Get all stops for a route.""" from app.models.route_stop import RouteStop from app.models.bus_stop import BusStop + from uuid import UUID + + try: + route_id_uuid = UUID(route_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid route ID format") statement = select(RouteStop, BusStop).join( BusStop, RouteStop.stop_id == BusStop.id - ).where(RouteStop.route_id == route_id).order_by(RouteStop.stop_order) + ).where(RouteStop.route_id == route_id_uuid).order_by(RouteStop.stop_order) results = session.exec(statement).all() # Merge RouteStop data into BusStop response stops = [] for route_stop, bus_stop in results: stop_data = bus_stop.model_dump() + # Convert UUIDs to strings for JSON compatibility + stop_data['id'] = str(stop_data['id']) stop_data['stop_order'] = route_stop.stop_order stop_data['travel_time_minutes'] = route_stop.travel_time_minutes stop_data['stop_delay_minutes'] = route_stop.stop_delay_minutes @@ -124,21 +132,28 @@ async def add_stop_to_route( _: bool = Depends(get_current_admin) ): """Add a stop to a route with cascading order adjustment.""" + from uuid import UUID + try: + route_id_uuid = UUID(route_id) + stop_id_uuid = UUID(stop_data.stop_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid UUID format") + # 1. Check if route exists - route = session.get(Route, route_id) + route = session.get(Route, route_id_uuid) if not route: raise HTTPException(status_code=404, detail="Route not found") # 2. Determine stop order if stop_data.stop_order is None: # Append to end - max_order = session.exec(select(func.max(RouteStop.stop_order)).where(RouteStop.route_id == route_id)).one() + max_order = session.exec(select(func.max(RouteStop.stop_order)).where(RouteStop.route_id == route_id_uuid)).one() # handle case where max_order is None (no stops) stop_data.stop_order = (max_order or 0) + 1 else: # Shift existing stops equal to or greater than new order existing_stops = session.exec( - select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_order >= stop_data.stop_order) + select(RouteStop).where(RouteStop.route_id == route_id_uuid, RouteStop.stop_order >= stop_data.stop_order) ).all() for stop in existing_stops: stop.stop_order += 1 @@ -146,8 +161,8 @@ async def add_stop_to_route( # 3. Create new RouteStop new_stop = RouteStop( - route_id=route_id, - stop_id=stop_data.stop_id, + route_id=route_id_uuid, + stop_id=stop_id_uuid, stop_order=stop_data.stop_order, travel_time_minutes=stop_data.travel_time_minutes, stop_delay_minutes=stop_data.stop_delay_minutes or 0, @@ -168,12 +183,14 @@ async def update_route_stop_order( _: bool = Depends(get_current_admin) ): """Update a route stop, potentially reordering others.""" - # This assumes we find the connection by route_id and stop_id. - # NOTE: If a stop is on a route multiple times, this logic needs ID, but for now assuming unique stop per route. - # Actually RouteStop has its own ID but we are using stop_id in path. Let's find the RouteStop entry. - + from uuid import UUID + try: + route_id_uuid = UUID(route_id) + stop_id_uuid = UUID(stop_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid UUID format") route_stop = session.exec( - select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_id == stop_id) + select(RouteStop).where(RouteStop.route_id == route_id_uuid, RouteStop.stop_id == stop_id_uuid) ).first() if not route_stop: @@ -199,7 +216,7 @@ async def update_route_stop_order( # Moving down: shift stops between old+1 and new DOWN (-1) stops_to_shift = session.exec( select(RouteStop).where( - RouteStop.route_id == route_id, + RouteStop.route_id == route_id_uuid, RouteStop.stop_order > old_order, RouteStop.stop_order <= new_order ) @@ -211,7 +228,7 @@ async def update_route_stop_order( # Moving up: shift stops between new and old-1 UP (+1) stops_to_shift = session.exec( select(RouteStop).where( - RouteStop.route_id == route_id, + RouteStop.route_id == route_id_uuid, RouteStop.stop_order >= new_order, RouteStop.stop_order < old_order ) @@ -227,3 +244,45 @@ async def update_route_stop_order( session.refresh(route_stop) return route_stop + +@router.delete("/{route_id}/stops/{stop_id}") +async def remove_stop_from_route( + route_id: str, + stop_id: str, + session: Session = Depends(get_session), + _: bool = Depends(get_current_admin) +): + """Remove a stop from a route.""" + from uuid import UUID + try: + route_id_uuid = UUID(route_id) + stop_id_uuid = UUID(stop_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid UUID format") + + route_stop = session.exec( + select(RouteStop).where( + RouteStop.route_id == route_id_uuid, + RouteStop.stop_id == stop_id_uuid + ) + ).first() + + if not route_stop: + raise HTTPException(status_code=404, detail="Stop not found on this route") + + # Re-order remaining stops + removed_order = route_stop.stop_order + remaining_stops = session.exec( + select(RouteStop).where( + RouteStop.route_id == route_id_uuid, + RouteStop.stop_order > removed_order + ) + ).all() + for s in remaining_stops: + s.stop_order -= 1 + session.add(s) + + session.delete(route_stop) + session.commit() + return {"ok": True} + diff --git a/backend/app/api/shuttles/__init__.py b/backend/app/api/shuttles/__init__.py index a70b26e..cae9da0 100644 --- a/backend/app/api/shuttles/__init__.py +++ b/backend/app/api/shuttles/__init__.py @@ -8,9 +8,9 @@ from app.core.database import get_session from app.models.shuttle import Shuttle from app.api.deps import get_current_admin -router = APIRouter(prefix="/api/shuttles", tags=["shuttles"]) +from app.services.image_handler import save_image, delete_image -UPLOAD_DIR = "uploads" +router = APIRouter(prefix="/api/shuttles", tags=["shuttles"]) @router.get("", response_model=List[Shuttle]) async def get_shuttles( @@ -63,12 +63,7 @@ async def create_shuttle( """Create a new shuttle trip (Admin only).""" image_url = None if image: - ext = os.path.splitext(image.filename)[1] - filename = f"{uuid4()}{ext}" - path = os.path.join(UPLOAD_DIR, "vehicles", filename) - with open(path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) - image_url = f"/uploads/vehicles/{filename}" + image_url = save_image(image, "vehicles") shuttle = Shuttle( route_name=route_name, @@ -137,12 +132,10 @@ async def update_shuttle( db_shuttle.is_active = is_active if image: - ext = os.path.splitext(image.filename)[1] - filename = f"{uuid4()}{ext}" - path = os.path.join(UPLOAD_DIR, "vehicles", filename) - with open(path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) - db_shuttle.image_url = f"/uploads/vehicles/{filename}" + # Delete old image if exists + if db_shuttle.image_url: + delete_image(db_shuttle.image_url) + db_shuttle.image_url = save_image(image, "vehicles") session.add(db_shuttle) session.commit() @@ -159,6 +152,11 @@ async def delete_shuttle( db_shuttle = session.get(Shuttle, shuttle_id) if not db_shuttle: raise HTTPException(status_code=404, detail="Shuttle not found") + + # Delete image from storage + if db_shuttle.image_url: + delete_image(db_shuttle.image_url) + session.delete(db_shuttle) session.commit() return {"ok": True} diff --git a/backend/app/api/taxis/__init__.py b/backend/app/api/taxis/__init__.py index 8810555..81c4e1b 100644 --- a/backend/app/api/taxis/__init__.py +++ b/backend/app/api/taxis/__init__.py @@ -8,11 +8,10 @@ from app.core.database import get_session from app.models.taxi import Taxi from app.api.deps import get_current_admin +from app.services.image_handler import save_image, delete_image + router = APIRouter(prefix="/api/taxis", tags=["taxis"]) -UPLOAD_DIR = "uploads" - - @router.get("", response_model=List[Taxi]) async def get_taxis( corregimiento: Optional[str] = Query(None), @@ -62,12 +61,7 @@ async def create_taxi( """Create a new taxi entry (Admin only).""" image_url = None if image: - ext = os.path.splitext(image.filename)[1] - filename = f"{uuid4()}{ext}" - path = os.path.join(UPLOAD_DIR, "profiles", filename) - with open(path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) - image_url = f"/uploads/profiles/{filename}" + image_url = save_image(image, "profiles") taxi = Taxi( owner_name=owner_name, @@ -121,12 +115,9 @@ async def update_taxi( # Handle image upload if image: - ext = os.path.splitext(image.filename)[1] - filename = f"{uuid4()}{ext}" - path = os.path.join(UPLOAD_DIR, "profiles", filename) - with open(path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) - db_taxi.image_url = f"/uploads/profiles/{filename}" + if db_taxi.image_url: + delete_image(db_taxi.image_url) + db_taxi.image_url = save_image(image, "profiles") session.add(db_taxi) session.commit() @@ -144,6 +135,10 @@ async def delete_taxi( db_taxi = session.get(Taxi, taxi_id) if not db_taxi: raise HTTPException(status_code=404, detail="Taxi not found") + + if db_taxi.image_url: + delete_image(db_taxi.image_url) + session.delete(db_taxi) session.commit() return {"ok": True} diff --git a/backend/app/main.py b/backend/app/main.py index 6651e2d..c2c31d4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -4,6 +4,10 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles import os +# Absolute path for uploads directory (relative to this file: app/main.py -> backend/uploads) +_BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +UPLOAD_BASE = os.path.join(_BACKEND_DIR, "uploads") + from app.core.config import settings from app.core.database import init_db, engine from sqlmodel import Session, select @@ -31,6 +35,8 @@ from alembic import command import random from datetime import datetime, timedelta +from app.services.image_handler import cleanup_expired_coupons + @asynccontextmanager async def lifespan(app: FastAPI): # Run migrations @@ -46,6 +52,13 @@ async def lifespan(app: FastAPI): except: pass + # Run cleanup of expired coupons + try: + with Session(engine) as session: + cleanup_expired_coupons(session) + except Exception as e: + print(f"WARNING: Initial cleanup failed: {e}") + # Seed sample data if empty with Session(engine) as session: # 1. Taxis @@ -96,7 +109,7 @@ async def lifespan(app: FastAPI): biz = Business(name=spot["name"], address=f"Sector {spot['area']}", phone="6000-0000", category="Area Turistica", area=spot["area"], latitude=spot["lat"], longitude=spot["lng"]) session.add(biz) session.flush() - coupon = Coupon(title=f"Oferta en {biz.name}", description=f"Descuento especial en {biz.name}", business_id=biz.id, business_name=biz.name, business_address=biz.address, business_phone=biz.phone, category=biz.category, discount_percentage=15, valid_from=datetime.now().isoformat(), valid_until=(datetime.now() + timedelta(days=30)).isoformat(), is_active=True) + coupon = Coupon(title=f"Oferta en {biz.name}", description=f"Descuento especial en {biz.name}", business_id=biz.id, business_name=biz.name, business_address=biz.address, business_phone=biz.phone, category=biz.category, discount_percentage=15, valid_from=datetime.now(), valid_until=(datetime.now() + timedelta(days=30)), is_active=True) session.add(coupon) session.commit() yield @@ -128,11 +141,11 @@ app.add_middleware( ) # Ensure upload directories exist -for sub in ["profiles", "vehicles", "businesses"]: - os.makedirs(os.path.join("uploads", sub), exist_ok=True) +for sub in ["profiles", "vehicles", "businesses", "coupons"]: + os.makedirs(os.path.join(UPLOAD_BASE, sub), exist_ok=True) # Mount static files -app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") +app.mount("/uploads", StaticFiles(directory=UPLOAD_BASE), name="uploads") # Include routers app.include_router(routes_router) diff --git a/backend/app/services/image_handler.py b/backend/app/services/image_handler.py new file mode 100644 index 0000000..458fa7a --- /dev/null +++ b/backend/app/services/image_handler.py @@ -0,0 +1,79 @@ +import os +import shutil +from uuid import uuid4 +from fastapi import UploadFile +from typing import Optional +from datetime import datetime +from sqlmodel import Session, select +from app.models.coupon import Coupon + +# Use absolute path relative to THIS file's location (app/services/image_handler.py) +# Going up 3 levels: services -> app -> backend -> uploads +_BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +UPLOAD_BASE = os.path.join(_BASE_DIR, "uploads") + + +def save_image(image: UploadFile, subfolder: str) -> str: + """Saves an image to the local filesystem and returns its relative URL.""" + upload_dir = os.path.join(UPLOAD_BASE, subfolder) + os.makedirs(upload_dir, exist_ok=True) + + # Generate unique filename + ext = os.path.splitext(image.filename or "")[1].lower() + if not ext or ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif']: + ext = ".jpg" # Default extension if missing or invalid + + filename = f"{uuid4()}{ext}" + path = os.path.join(upload_dir, filename) + + # Reset file pointer to start before reading + image.file.seek(0) + + # Save file + with open(path, "wb") as buffer: + shutil.copyfileobj(image.file, buffer) + + print(f"DEBUG: Saved image to {path}") + return f"/uploads/{subfolder}/{filename}" + + +def delete_image(image_url: Optional[str]): + """Deletes an image from the filesystem if it's stored locally.""" + if not image_url: + return + + # We only delete if it points to our local uploads folder + if image_url.startswith("/uploads/"): + # Build absolute path + relative = image_url.lstrip("/") # e.g. "uploads/businesses/uuid.jpg" + file_path = os.path.join(_BASE_DIR, relative) + if os.path.exists(file_path): + try: + os.remove(file_path) + print(f"DEBUG: Deleted file {file_path}") + except Exception as e: + print(f"ERROR: Failed to delete {file_path}: {e}") + else: + print(f"DEBUG: File not found for deletion: {file_path}") + + +def cleanup_expired_coupons(session: Session): + """Finds and deletes expired coupons and their associated images.""" + try: + now = datetime.now() + # Coupons where valid_until is passed + statement = select(Coupon).where(Coupon.valid_until < now) + expired = session.exec(statement).all() + + count = 0 + for coupon in expired: + if coupon.image_url: + delete_image(coupon.image_url) + session.delete(coupon) + count += 1 + + if count > 0: + session.commit() + print(f"DEBUG: Cleaned up {count} expired coupons and their images.") + except Exception as e: + print(f"ERROR: Cleanup failed: {e}") diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6f10a83..a87bc5b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,12 +4,16 @@ import { RouterView, useRoute } from "vue-router"; import { useI18n } from 'vue-i18n' import MainLayout from "./components/layouts/MainLayout.vue"; import { useThemeStore } from './stores/theme' +import { useAuthStore } from './stores/auth' +import { useFavoritesStore } from './stores/favorites' import { analyticsService } from '@/services/analyticsService' // Initialize theme store const route = useRoute() const { locale } = useI18n() const themeStore = useThemeStore() +const authStore = useAuthStore() +const favoritesStore = useFavoritesStore() const isSplashScreen = computed(() => route.name === 'splash') const isAuthScreen = computed(() => route.name === 'auth' || route.path === '/login') @@ -20,6 +24,10 @@ onMounted(() => { event_name: 'app_open', properties: { language: locale.value } }) + // Load favorites if the user is already logged in + if (authStore.isAuthenticated) { + favoritesStore.loadFavorites() + } }) diff --git a/frontend/src/components/BottomNav.vue b/frontend/src/components/BottomNav.vue index 5df1937..1f1e0ec 100644 --- a/frontend/src/components/BottomNav.vue +++ b/frontend/src/components/BottomNav.vue @@ -14,12 +14,20 @@ const navItems = [ { name: 'taxi', path: '/taxi', icon: 'directions_bus' } ] +let isNavigating = false + const navigateTo = (path: string) => { - router.push(path) + // Prevent rapid multiple navigations (debounce guard) + if (isNavigating) return + if (route.path === path) return + isNavigating = true + router.push(path).finally(() => { + setTimeout(() => { isNavigating = false }, 300) + }) } const isActive = (path: string) => { - return route.path === path + return route.path === path || route.path.startsWith(path + '/') } // Scroll detection logic diff --git a/frontend/src/utils/imageUrl.ts b/frontend/src/utils/imageUrl.ts new file mode 100644 index 0000000..6834466 --- /dev/null +++ b/frontend/src/utils/imageUrl.ts @@ -0,0 +1,38 @@ +import { API_URL } from '@/services/apiClient' + +/** + * Returns a full URL for an image path. + * Handles null/undefined paths, absolute URLs, and relative backend paths. + */ +export function getImageUrl(path?: string | null, type: 'taxi' | 'shuttle' | 'business' | 'coupon' = 'business') { + if (!path) { + const defaultNames: Record = { + taxi: 'Taxi', + shuttle: 'Transporte', + business: 'Negocio', + coupon: 'Oferta' + } + const name = defaultNames[type] || 'SIBU' + return `https://ui-avatars.com/api/?name=${name}&background=fee715&color=101820&size=256&bold=true` + } + + if (path.startsWith('http')) { + return path + } + + // Ensure path starts with / for joining with API_URL + const cleanPath = path.startsWith('/') ? path : `/${path}` + + // Ensure API_URL includes protocol if missing (safety check) + let cleanBaseUrl = API_URL.trim() + if (!cleanBaseUrl.startsWith('http')) { + cleanBaseUrl = `https://${cleanBaseUrl}` + } + + // Remove trailing slash from base URL + if (cleanBaseUrl.endsWith('/')) { + cleanBaseUrl = cleanBaseUrl.slice(0, -1) + } + + return `${cleanBaseUrl}${cleanPath}` +} diff --git a/frontend/src/views/AdminShuttles.vue b/frontend/src/views/AdminShuttles.vue index 1548a82..1c46cf5 100644 --- a/frontend/src/views/AdminShuttles.vue +++ b/frontend/src/views/AdminShuttles.vue @@ -211,7 +211,8 @@ async function saveShuttle() {
-
+
+
@@ -516,12 +517,11 @@ async function saveShuttle() { border-radius: 20px; overflow: hidden; border: 1px solid rgba(255,255,255,0.12); - background-size: cover; - background-position: center; position: relative; display: flex; flex-direction: column; box-shadow: 0 30px 60px rgba(0,0,0,0.5); + background: #101820; } .shuttle-card-preview::before { @@ -534,7 +534,7 @@ async function saveShuttle() { rgba(0, 0, 0, 0.65) 55%, rgba(0, 0, 0, 0.30) 100% ); - z-index: 0; + z-index: 1; } .shuttle-card-preview.expanded { @@ -553,7 +553,7 @@ async function saveShuttle() { .shuttle-main-info, .shuttle-details { position: relative; - z-index: 1; + z-index: 2; padding: 18px 20px; } diff --git a/frontend/src/views/DiscoverView.vue b/frontend/src/views/DiscoverView.vue index a9ac4db..5d93ad8 100644 --- a/frontend/src/views/DiscoverView.vue +++ b/frontend/src/views/DiscoverView.vue @@ -1,11 +1,11 @@