auto-commit for 8a62051e-b038-4363-a62d-7047e3d6e102

This commit is contained in:
emergent-agent-e1
2026-03-16 18:22:14 +00:00
parent 706e3e2eb6
commit 6a9e343332
16 changed files with 1510 additions and 212 deletions
View File
+57
View File
@@ -0,0 +1,57 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, Security, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import os
# JWT Configuration
SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "your-secret-key-change-in-production-epic-travel-2025")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 hours
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Security
security = HTTPBearer()
def hash_password(password: str) -> str:
"""Hash a password"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""Create a JWT access token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> dict:
"""Decode and verify a JWT token"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Could not validate credentials")
async def get_current_admin(credentials: HTTPAuthorizationCredentials = Security(security)) -> dict:
"""Dependency to get current admin from JWT token"""
token = credentials.credentials
payload = decode_access_token(token)
email: str = payload.get("sub")
if email is None:
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
return {"email": email}
+1
View File
@@ -0,0 +1 @@
# Models module
+85
View File
@@ -0,0 +1,85 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
import uuid
class Destination(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
location: str
description: str
image: str
category: str # City, Beach, Adventure
rating: float
price: float
currency: str = "USD"
created_at: datetime = Field(default_factory=datetime.utcnow)
class DestinationCreate(BaseModel):
name: str
location: str
description: str
image: str
category: str
rating: float
price: float
currency: str = "USD"
class DestinationUpdate(BaseModel):
name: Optional[str] = None
location: Optional[str] = None
description: Optional[str] = None
image: Optional[str] = None
category: Optional[str] = None
rating: Optional[float] = None
price: Optional[float] = None
currency: Optional[str] = None
class Special(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
destination_id: str
discount: float
end_date: str # ISO format date
highlights: List[str]
created_at: datetime = Field(default_factory=datetime.utcnow)
class SpecialCreate(BaseModel):
destination_id: str
discount: float
end_date: str
highlights: List[str]
class SpecialUpdate(BaseModel):
discount: Optional[float] = None
end_date: Optional[str] = None
highlights: Optional[List[str]] = None
class AdminUser(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
email: str
password_hash: str
created_at: datetime = Field(default_factory=datetime.utcnow)
class AdminLogin(BaseModel):
email: str
password: str
class Contact(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
email: str
message: str
created_at: datetime = Field(default_factory=datetime.utcnow)
class ContactCreate(BaseModel):
name: str
email: str
message: str
class NewsletterSubscriber(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
email: str
subscribed_at: datetime = Field(default_factory=datetime.utcnow)
class NewsletterSubscribe(BaseModel):
email: str
+121 -25
View File
@@ -1,27 +1,123 @@
fastapi==0.110.1
uvicorn==0.25.0
boto3>=1.34.129
requests-oauthlib>=2.0.0
cryptography>=42.0.8
python-dotenv>=1.0.1
pymongo==4.5.0
pydantic>=2.6.4
email-validator>=2.2.0
pyjwt>=2.10.1
aiohappyeyeballs==2.6.1
aiohttp==3.13.3
aiosignal==1.4.0
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.1
attrs==25.4.0
bcrypt==4.1.3
passlib>=1.7.4
tzdata>=2024.2
motor==3.3.1
pytest>=8.0.0
black>=24.1.1
isort>=5.13.2
flake8>=7.0.0
mypy>=1.8.0
python-jose>=3.3.0
requests>=2.31.0
pandas>=2.2.0
numpy>=1.26.0
python-multipart>=0.0.9
jq>=1.6.0
typer>=0.9.0
black==26.1.0
boto3==1.42.58
botocore==1.42.58
certifi==2026.2.25
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
cryptography==46.0.5
distro==1.9.0
dnspython==2.8.0
ecdsa==0.19.1
email-validator==2.3.0
emergentintegrations==0.1.0
fastapi==0.110.1
fastuuid==0.14.0
filelock==3.25.0
flake8==7.3.0
frozenlist==1.8.0
fsspec==2026.2.0
google-ai-generativelanguage==0.6.15
google-api-core==2.30.0
google-api-python-client==2.191.0
google-auth==2.49.0.dev0
google-auth-httplib2==0.3.0
google-genai==1.65.0
google-generativeai==0.8.6
googleapis-common-protos==1.72.0
grpcio==1.78.0
grpcio-status==1.71.2
h11==0.16.0
hf-xet==1.3.2
httpcore==1.0.9
httplib2==0.31.2
httpx==0.28.1
huggingface_hub==1.5.0
idna==3.11
importlib_metadata==8.7.1
iniconfig==2.3.0
isort==8.0.0
Jinja2==3.1.6
jiter==0.13.0
jmespath==1.1.0
jq==1.11.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
librt==0.8.1
litellm==1.80.0
markdown-it-py==4.0.0
MarkupSafe==3.0.3
mccabe==0.7.0
mdurl==0.1.2
motor==3.3.1
multidict==6.7.1
mypy==1.19.1
mypy_extensions==1.1.0
numpy==2.4.2
oauthlib==3.3.1
openai==1.99.9
packaging==26.0
pandas==3.0.1
passlib==1.7.4
pathspec==1.0.4
pillow==12.1.1
platformdirs==4.9.2
pluggy==1.6.0
propcache==0.4.1
proto-plus==1.27.1
protobuf==5.29.6
pyasn1==0.6.2
pyasn1_modules==0.4.2
pycodestyle==2.14.0
pycparser==3.0
pydantic==2.12.5
pydantic_core==2.41.5
pyflakes==3.4.0
Pygments==2.19.2
PyJWT==2.11.0
pymongo==4.5.0
pyparsing==3.3.2
pytest==9.0.2
python-dateutil==2.9.0.post0
python-dotenv==1.2.1
python-jose==3.5.0
python-multipart==0.0.22
pytokens==0.4.1
PyYAML==6.0.3
referencing==0.37.0
regex==2026.2.28
requests==2.32.5
requests-oauthlib==2.0.0
rich==14.3.3
rpds-py==0.30.0
rsa==4.9.1
s3transfer==0.16.0
s5cmd==0.2.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
starlette==0.37.2
stripe==14.4.0
tenacity==9.1.4
tiktoken==0.12.0
tokenizers==0.22.2
tqdm==4.67.3
typer==0.24.1
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2025.3
uritemplate==4.2.0
urllib3==2.6.3
uvicorn==0.25.0
watchfiles==1.1.1
websockets==16.0
yarl==1.23.0
zipp==3.23.0
+1
View File
@@ -0,0 +1 @@
# Routes module
+61
View File
@@ -0,0 +1,61 @@
from fastapi import APIRouter, HTTPException, Depends
from models.schemas import AdminLogin
from auth import hash_password, verify_password, create_access_token
from motor.motor_asyncio import AsyncIOMotorClient
import os
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.post("/login")
async def login(credentials: AdminLogin):
"""Admin login endpoint"""
# Find admin user
admin = await db.admin_users.find_one({"email": credentials.email})
if not admin:
raise HTTPException(status_code=401, detail="Invalid email or password")
# Verify password
if not verify_password(credentials.password, admin["password_hash"]):
raise HTTPException(status_code=401, detail="Invalid email or password")
# Create access token
access_token = create_access_token(data={"sub": admin["email"]})
return {
"access_token": access_token,
"token_type": "bearer",
"email": admin["email"]
}
@router.post("/verify")
async def verify_token(admin: dict = Depends(lambda: __import__('auth').get_current_admin)):
"""Verify JWT token"""
return {"valid": True, "email": admin["email"]}
@router.post("/initialize-admin")
async def initialize_admin():
"""Initialize default admin user (for development/setup only)"""
# Check if admin already exists
existing_admin = await db.admin_users.find_one({"email": "admin@epictravel.com"})
if existing_admin:
return {"message": "Admin user already exists"}
# Create default admin
admin_data = {
"email": "admin@epictravel.com",
"password_hash": hash_password("admin123"),
"created_at": __import__('datetime').datetime.utcnow()
}
await db.admin_users.insert_one(admin_data)
return {"message": "Admin user created successfully", "email": "admin@epictravel.com"}
+113
View File
@@ -0,0 +1,113 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Optional
from models.schemas import Destination, DestinationCreate, DestinationUpdate
from auth import get_current_admin
import uuid
from datetime import datetime
router = APIRouter(prefix="/api/destinations", tags=["Destinations"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.get("", response_model=List[Destination])
async def get_destinations(category: Optional[str] = None, search: Optional[str] = None):
"""Get all destinations with optional filtering"""
query = {}
if category and category != "All":
query["category"] = category
if search:
query["$or"] = [
{"name": {"$regex": search, "$options": "i"}},
{"location": {"$regex": search, "$options": "i"}}
]
destinations = await db.destinations.find(query).to_list(1000)
# Convert MongoDB _id to id for response
for dest in destinations:
if "_id" in dest:
del dest["_id"]
return destinations
@router.get("/{destination_id}", response_model=Destination)
async def get_destination(destination_id: str):
"""Get a single destination by ID"""
destination = await db.destinations.find_one({"id": destination_id})
if not destination:
raise HTTPException(status_code=404, detail="Destination not found")
if "_id" in destination:
del destination["_id"]
return destination
@router.post("", response_model=Destination)
async def create_destination(
destination: DestinationCreate,
admin: dict = Depends(get_current_admin)
):
"""Create a new destination (admin only)"""
destination_data = destination.dict()
destination_data["id"] = str(uuid.uuid4())
destination_data["created_at"] = datetime.utcnow()
await db.destinations.insert_one(destination_data)
if "_id" in destination_data:
del destination_data["_id"]
return destination_data
@router.put("/{destination_id}", response_model=Destination)
async def update_destination(
destination_id: str,
destination_update: DestinationUpdate,
admin: dict = Depends(get_current_admin)
):
"""Update a destination (admin only)"""
# Check if destination exists
existing = await db.destinations.find_one({"id": destination_id})
if not existing:
raise HTTPException(status_code=404, detail="Destination not found")
# Update only provided fields
update_data = {k: v for k, v in destination_update.dict().items() if v is not None}
if update_data:
await db.destinations.update_one(
{"id": destination_id},
{"$set": update_data}
)
# Fetch updated destination
updated = await db.destinations.find_one({"id": destination_id})
if "_id" in updated:
del updated["_id"]
return updated
@router.delete("/{destination_id}")
async def delete_destination(
destination_id: str,
admin: dict = Depends(get_current_admin)
):
"""Delete a destination (admin only)"""
result = await db.destinations.delete_one({"id": destination_id})
if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Destination not found")
# Also delete any specials for this destination
await db.specials.delete_many({"destination_id": destination_id})
return {"message": "Destination deleted successfully"}
+77
View File
@@ -0,0 +1,77 @@
from fastapi import APIRouter, HTTPException, File, UploadFile
from fastapi.responses import FileResponse
from typing import List
from models.schemas import ContactCreate, NewsletterSubscribe
from datetime import datetime
import uuid
import os
import shutil
router = APIRouter(prefix="/api", tags=["Other"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.post("/contact")
async def submit_contact(contact: ContactCreate):
"""Submit a contact form"""
contact_data = contact.dict()
contact_data["id"] = str(uuid.uuid4())
contact_data["created_at"] = datetime.utcnow()
await db.contacts.insert_one(contact_data)
return {"message": "Contact form submitted successfully"}
@router.post("/newsletter/subscribe")
async def subscribe_newsletter(subscriber: NewsletterSubscribe):
"""Subscribe to newsletter"""
# Check if already subscribed
existing = await db.newsletter_subscribers.find_one({"email": subscriber.email})
if existing:
return {"message": "Email already subscribed"}
subscriber_data = subscriber.dict()
subscriber_data["id"] = str(uuid.uuid4())
subscriber_data["subscribed_at"] = datetime.utcnow()
await db.newsletter_subscribers.insert_one(subscriber_data)
return {"message": "Successfully subscribed to newsletter"}
@router.post("/upload/image")
async def upload_image(file: UploadFile = File(...)):
"""Upload an image file"""
# Validate file type
allowed_extensions = [".jpg", ".jpeg", ".png", ".webp"]
file_ext = os.path.splitext(file.filename)[1].lower()
if file_ext not in allowed_extensions:
raise HTTPException(status_code=400, detail="Invalid file type. Allowed: jpg, jpeg, png, webp")
# Generate unique filename
unique_filename = f"{uuid.uuid4()}{file_ext}"
file_path = f"/app/backend/uploads/{unique_filename}"
# Save file
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Return URL
file_url = f"/api/uploads/{unique_filename}"
return {"url": file_url, "filename": unique_filename}
@router.get("/uploads/{filename}")
async def get_uploaded_image(filename: str):
"""Serve uploaded images"""
file_path = f"/app/backend/uploads/{filename}"
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Image not found")
return FileResponse(file_path)
+109
View File
@@ -0,0 +1,109 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import List
from models.schemas import Special, SpecialCreate, SpecialUpdate
from auth import get_current_admin
import uuid
from datetime import datetime
router = APIRouter(prefix="/api/specials", tags=["Specials"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.get("", response_model=List[Special])
async def get_specials():
"""Get all weekly specials"""
specials = await db.specials.find().to_list(1000)
# Convert MongoDB _id to id for response
for special in specials:
if "_id" in special:
del special["_id"]
return specials
@router.get("/{special_id}", response_model=Special)
async def get_special(special_id: str):
"""Get a single special by ID"""
special = await db.specials.find_one({"id": special_id})
if not special:
raise HTTPException(status_code=404, detail="Special not found")
if "_id" in special:
del special["_id"]
return special
@router.post("", response_model=Special)
async def create_special(
special: SpecialCreate,
admin: dict = Depends(get_current_admin)
):
"""Add a destination to specials (admin only)"""
# Check if destination exists
destination = await db.destinations.find_one({"id": special.destination_id})
if not destination:
raise HTTPException(status_code=404, detail="Destination not found")
# Check if special already exists for this destination
existing = await db.specials.find_one({"destination_id": special.destination_id})
if existing:
raise HTTPException(status_code=400, detail="Special already exists for this destination")
special_data = special.dict()
special_data["id"] = str(uuid.uuid4())
special_data["created_at"] = datetime.utcnow()
await db.specials.insert_one(special_data)
if "_id" in special_data:
del special_data["_id"]
return special_data
@router.put("/{special_id}", response_model=Special)
async def update_special(
special_id: str,
special_update: SpecialUpdate,
admin: dict = Depends(get_current_admin)
):
"""Update a special (admin only)"""
# Check if special exists
existing = await db.specials.find_one({"id": special_id})
if not existing:
raise HTTPException(status_code=404, detail="Special not found")
# Update only provided fields
update_data = {k: v for k, v in special_update.dict().items() if v is not None}
if update_data:
await db.specials.update_one(
{"id": special_id},
{"$set": update_data}
)
# Fetch updated special
updated = await db.specials.find_one({"id": special_id})
if "_id" in updated:
del updated["_id"]
return updated
@router.delete("/destination/{destination_id}")
async def delete_special_by_destination(
destination_id: str,
admin: dict = Depends(get_current_admin)
):
"""Remove a destination from specials (admin only)"""
result = await db.specials.delete_one({"destination_id": destination_id})
if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Special not found for this destination")
return {"message": "Special removed successfully"}
+224 -52
View File
@@ -1,15 +1,15 @@
from fastapi import FastAPI, APIRouter
from fastapi import FastAPI
from dotenv import load_dotenv
from starlette.middleware.cors import CORSMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
import os
import logging
from pathlib import Path
from pydantic import BaseModel, Field, ConfigDict
from typing import List
import uuid
from datetime import datetime, timezone
from auth import hash_password
from datetime import datetime
# Import route modules
from routes import auth_routes, destination_routes, special_routes, other_routes
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
@@ -19,60 +19,30 @@ mongo_url = os.environ['MONGO_URL']
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ['DB_NAME']]
# Create the main app without a prefix
app = FastAPI()
# Inject database into route modules
auth_routes.set_db(db)
destination_routes.set_db(db)
special_routes.set_db(db)
other_routes.set_db(db)
# Create a router with the /api prefix
api_router = APIRouter(prefix="/api")
# Create the main app
app = FastAPI(title="Epic Travel & Destinations API")
# Include routers
app.include_router(auth_routes.router)
app.include_router(destination_routes.router)
app.include_router(special_routes.router)
app.include_router(other_routes.router)
# Define Models
class StatusCheck(BaseModel):
model_config = ConfigDict(extra="ignore") # Ignore MongoDB's _id field
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
client_name: str
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class StatusCheckCreate(BaseModel):
client_name: str
# Add your routes to the router instead of directly to app
@api_router.get("/")
# Health check endpoint
@app.get("/api")
async def root():
return {"message": "Hello World"}
@api_router.post("/status", response_model=StatusCheck)
async def create_status_check(input: StatusCheckCreate):
status_dict = input.model_dump()
status_obj = StatusCheck(**status_dict)
# Convert to dict and serialize datetime to ISO string for MongoDB
doc = status_obj.model_dump()
doc['timestamp'] = doc['timestamp'].isoformat()
_ = await db.status_checks.insert_one(doc)
return status_obj
@api_router.get("/status", response_model=List[StatusCheck])
async def get_status_checks():
# Exclude MongoDB's _id field from the query results
status_checks = await db.status_checks.find({}, {"_id": 0}).to_list(1000)
# Convert ISO string timestamps back to datetime objects
for check in status_checks:
if isinstance(check['timestamp'], str):
check['timestamp'] = datetime.fromisoformat(check['timestamp'])
return status_checks
# Include the router in the main app
app.include_router(api_router)
return {"message": "Epic Travel API is running", "status": "healthy"}
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=os.environ.get('CORS_ORIGINS', '*').split(','),
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@@ -84,6 +54,208 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
@app.on_event("startup")
async def startup_db_client():
"""Initialize database with seed data if empty"""
try:
# Check if admin user exists, if not create one
admin_exists = await db.admin_users.find_one({"email": "admin@epictravel.com"})
if not admin_exists:
admin_data = {
"id": "admin-1",
"email": "admin@epictravel.com",
"password_hash": hash_password("admin123"),
"created_at": datetime.utcnow()
}
await db.admin_users.insert_one(admin_data)
logger.info("Default admin user created")
# Check if destinations exist, if not seed initial data
dest_count = await db.destinations.count_documents({})
if dest_count == 0:
# Seed initial destinations
initial_destinations = [
{
"id": "1",
"name": "Paris",
"location": "France",
"description": "Experience the romance and elegance of the City of Light. Visit iconic landmarks like the Eiffel Tower, Louvre Museum, and stroll along the Champs-Élysées.",
"image": "https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&q=80",
"category": "City",
"rating": 4.9,
"price": 1299,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "2",
"name": "Bali",
"location": "Indonesia",
"description": "Discover tropical paradise with stunning beaches, ancient temples, lush rice terraces, and vibrant culture in this Indonesian gem.",
"image": "https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&q=80",
"category": "Beach",
"rating": 4.8,
"price": 899,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "3",
"name": "Tokyo",
"location": "Japan",
"description": "Immerse yourself in the perfect blend of ancient tradition and cutting-edge technology in Japan's bustling capital city.",
"image": "https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800&q=80",
"category": "City",
"rating": 4.9,
"price": 1499,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "4",
"name": "Santorini",
"location": "Greece",
"description": "Marvel at breathtaking sunsets, whitewashed buildings, and crystal-clear waters in this stunning Greek island paradise.",
"image": "https://images.unsplash.com/photo-1613395877344-13d4a8e0d49e?w=800&q=80",
"category": "Beach",
"rating": 4.9,
"price": 1199,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "5",
"name": "Iceland",
"location": "Iceland",
"description": "Witness the Northern Lights, explore glaciers, geysers, and volcanic landscapes in this land of fire and ice.",
"image": "https://images.unsplash.com/photo-1504829857797-ddff29c27927?w=800&q=80",
"category": "Adventure",
"rating": 4.8,
"price": 1699,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "6",
"name": "Dubai",
"location": "UAE",
"description": "Experience luxury and innovation in the desert with world-class shopping, stunning architecture, and endless entertainment.",
"image": "https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=800&q=80",
"category": "City",
"rating": 4.7,
"price": 1399,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "7",
"name": "Maldives",
"location": "Maldives",
"description": "Relax in overwater bungalows, dive in pristine coral reefs, and enjoy the ultimate tropical island getaway.",
"image": "https://images.unsplash.com/photo-1514282401047-d79a71a590e8?w=800&q=80",
"category": "Beach",
"rating": 5.0,
"price": 2199,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "8",
"name": "New York",
"location": "USA",
"description": "Explore the city that never sleeps with iconic landmarks, world-class museums, Broadway shows, and diverse neighborhoods.",
"image": "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&q=80",
"category": "City",
"rating": 4.8,
"price": 1099,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "9",
"name": "Machu Picchu",
"location": "Peru",
"description": "Trek to the ancient Incan citadel nestled high in the Andes Mountains, one of the New Seven Wonders of the World.",
"image": "https://images.unsplash.com/photo-1587595431973-160d0d94add1?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 1299,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "10",
"name": "Swiss Alps",
"location": "Switzerland",
"description": "Ski pristine slopes, hike mountain trails, and enjoy charming alpine villages with breathtaking mountain vistas.",
"image": "https://images.unsplash.com/photo-1531366936337-7c912a4589a7?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 1799,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "11",
"name": "Venice",
"location": "Italy",
"description": "Glide through romantic canals, admire Renaissance architecture, and savor authentic Italian cuisine in this unique floating city.",
"image": "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=800&q=80",
"category": "City",
"rating": 4.8,
"price": 1149,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "12",
"name": "Safari Kenya",
"location": "Kenya",
"description": "Witness the Great Migration, spot the Big Five, and experience the raw beauty of African wilderness.",
"image": "https://images.unsplash.com/photo-1516426122078-c23e76319801?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 2499,
"currency": "USD",
"created_at": datetime.utcnow()
}
]
await db.destinations.insert_many(initial_destinations)
logger.info(f"Seeded {len(initial_destinations)} initial destinations")
# Seed initial specials
initial_specials = [
{
"id": "special-1",
"destination_id": "2",
"discount": 25,
"end_date": "2025-02-28",
"highlights": ["Free spa treatment", "Complimentary airport transfer", "Sunset dinner cruise"],
"created_at": datetime.utcnow()
},
{
"id": "special-2",
"destination_id": "4",
"discount": 30,
"end_date": "2025-03-15",
"highlights": ["Wine tasting tour", "Private yacht excursion", "Luxury accommodation upgrade"],
"created_at": datetime.utcnow()
},
{
"id": "special-3",
"destination_id": "7",
"discount": 20,
"end_date": "2025-02-20",
"highlights": ["Snorkeling adventure", "Couples massage", "Romantic beach dinner"],
"created_at": datetime.utcnow()
}
]
await db.specials.insert_many(initial_specials)
logger.info(f"Seeded {len(initial_specials)} initial specials")
except Exception as e:
logger.error(f"Error during startup: {str(e)}")
@app.on_event("shutdown")
async def shutdown_db_client():
client.close()
client.close()
+291
View File
@@ -0,0 +1,291 @@
#!/usr/bin/env python3
import requests
import sys
import json
from datetime import datetime
class EpicTravelAPITester:
def __init__(self, base_url="https://world-explorer-139.preview.emergentagent.com"):
self.base_url = base_url
self.api_url = f"{base_url}/api"
self.token = None
self.tests_run = 0
self.tests_passed = 0
self.test_results = []
def log_test(self, name, success, message="", response_data=None):
"""Log test results"""
self.tests_run += 1
if success:
self.tests_passed += 1
print(f"{name}: PASSED - {message}")
else:
print(f"{name}: FAILED - {message}")
self.test_results.append({
"test": name,
"status": "PASSED" if success else "FAILED",
"message": message,
"response_data": response_data
})
return success
def run_test(self, name, method, endpoint, expected_status, data=None, headers=None):
"""Run a single API test"""
url = f"{self.api_url}/{endpoint}"
test_headers = {'Content-Type': 'application/json'}
if self.token:
test_headers['Authorization'] = f'Bearer {self.token}'
if headers:
test_headers.update(headers)
try:
if method == 'GET':
response = requests.get(url, headers=test_headers)
elif method == 'POST':
response = requests.post(url, json=data, headers=test_headers)
elif method == 'PUT':
response = requests.put(url, json=data, headers=test_headers)
elif method == 'DELETE':
response = requests.delete(url, headers=test_headers)
success = response.status_code == expected_status
if success:
try:
response_data = response.json() if response.content else {}
return self.log_test(name, True, f"Status: {response.status_code}", response_data), response_data
except:
return self.log_test(name, True, f"Status: {response.status_code}", {}), {}
else:
try:
error_data = response.json() if response.content else {}
return self.log_test(name, False, f"Expected {expected_status}, got {response.status_code} - {error_data}"), {}
except:
return self.log_test(name, False, f"Expected {expected_status}, got {response.status_code}"), {}
except Exception as e:
return self.log_test(name, False, f"Error: {str(e)}"), {}
def test_health_check(self):
"""Test API health check"""
success, data = self.run_test("API Health Check", "GET", "", 200)
return success
def test_admin_login(self):
"""Test admin login"""
login_data = {
"email": "admin@epictravel.com",
"password": "admin123"
}
success, response = self.run_test("Admin Login", "POST", "auth/login", 200, login_data)
if success and 'access_token' in response:
self.token = response['access_token']
self.log_test("Token Extraction", True, "JWT token successfully extracted")
return True
else:
self.log_test("Token Extraction", False, "Failed to extract JWT token")
return False
def test_get_destinations(self):
"""Test getting all destinations"""
success, data = self.run_test("Get All Destinations", "GET", "destinations", 200)
if success and isinstance(data, list) and len(data) > 0:
self.log_test("Destinations Data Validation", True, f"Found {len(data)} destinations")
return data
else:
self.log_test("Destinations Data Validation", False, "No destinations returned or invalid data")
return []
def test_get_specials(self):
"""Test getting all specials"""
success, data = self.run_test("Get All Specials", "GET", "specials", 200)
if success and isinstance(data, list):
self.log_test("Specials Data Validation", True, f"Found {len(data)} specials")
return data
else:
self.log_test("Specials Data Validation", False, "Failed to get specials or invalid data")
return []
def test_search_destinations(self):
"""Test destination search functionality"""
# Test search by name
success, data = self.run_test("Search Destinations by Name", "GET", "destinations?search=Paris", 200)
if success:
found_paris = any(dest.get('name', '').lower() == 'paris' for dest in data)
if found_paris:
self.log_test("Search Results Validation", True, "Paris found in search results")
else:
self.log_test("Search Results Validation", False, "Paris not found in search results")
# Test filter by category
success, data = self.run_test("Filter Destinations by Category", "GET", "destinations?category=City", 200)
if success:
all_city = all(dest.get('category') == 'City' for dest in data)
if all_city:
self.log_test("Filter Results Validation", True, "All returned destinations are City type")
else:
self.log_test("Filter Results Validation", False, "Filter not working correctly")
def test_contact_form(self):
"""Test contact form submission"""
contact_data = {
"name": "Test User",
"email": "test@example.com",
"message": "This is a test contact message"
}
success, data = self.run_test("Contact Form Submission", "POST", "contact", 200, contact_data)
return success
def test_newsletter_subscription(self):
"""Test newsletter subscription"""
newsletter_data = {
"email": "newsletter@test.com"
}
success, data = self.run_test("Newsletter Subscription", "POST", "newsletter/subscribe", 200, newsletter_data)
return success
def test_admin_crud_operations(self):
"""Test full CRUD operations for destinations (requires authentication)"""
if not self.token:
self.log_test("CRUD Operations", False, "No authentication token available")
return False
# Test creating a new destination
new_destination = {
"name": "Test Destination",
"location": "Test Country",
"description": "A test destination for API testing",
"image": "https://via.placeholder.com/800x600",
"category": "City",
"rating": 4.5,
"price": 999,
"currency": "USD"
}
success, created_dest = self.run_test("Create New Destination", "POST", "destinations", 200, new_destination)
if not success:
return False
dest_id = created_dest.get('id')
if not dest_id:
self.log_test("Destination ID Extraction", False, "Failed to get destination ID from creation response")
return False
# Test updating the destination
update_data = {
"name": "Updated Test Destination",
"price": 1299
}
success, updated_dest = self.run_test("Update Destination", "PUT", f"destinations/{dest_id}", 200, update_data)
if success and updated_dest.get('name') == "Updated Test Destination":
self.log_test("Update Validation", True, "Destination updated successfully")
else:
self.log_test("Update Validation", False, "Destination update failed or data not persisted")
# Test getting single destination
success, single_dest = self.run_test("Get Single Destination", "GET", f"destinations/{dest_id}", 200)
# Test adding destination to specials
special_data = {
"destination_id": dest_id,
"discount": 25,
"end_date": "2025-12-31",
"highlights": ["Test special", "Limited time", "API test"]
}
success, created_special = self.run_test("Add Destination to Specials", "POST", "specials", 200, special_data)
special_id = created_special.get('id') if success else None
if special_id:
# Test updating special
special_update = {
"discount": 30,
"end_date": "2025-11-30"
}
success, updated_special = self.run_test("Update Special", "PUT", f"specials/{special_id}", 200, special_update)
# Test removing destination from specials
success, _ = self.run_test("Remove from Specials", "DELETE", f"specials/destination/{dest_id}", 200)
# Test deleting the destination (cleanup)
success, _ = self.run_test("Delete Test Destination", "DELETE", f"destinations/{dest_id}", 200)
return True
def run_all_tests(self):
"""Run all tests in sequence"""
print("🚀 Starting Epic Travel API Testing...")
print("=" * 50)
# Test 1: Health Check
print("\n📋 Testing Basic API Access...")
if not self.test_health_check():
print("❌ Health check failed, stopping tests")
return self.generate_summary()
# Test 2: Authentication
print("\n🔐 Testing Authentication...")
if not self.test_admin_login():
print("❌ Authentication failed, continuing with public endpoints only")
# Test 3: Public Endpoints
print("\n🌍 Testing Public Endpoints...")
destinations = self.test_get_destinations()
specials = self.test_get_specials()
self.test_search_destinations()
# Test 4: Forms
print("\n📝 Testing Form Submissions...")
self.test_contact_form()
self.test_newsletter_subscription()
# Test 5: Admin Operations
if self.token:
print("\n⚙️ Testing Admin CRUD Operations...")
self.test_admin_crud_operations()
else:
print("\n⚠️ Skipping Admin CRUD tests - No authentication token")
return self.generate_summary()
def generate_summary(self):
"""Generate test summary"""
print("\n" + "=" * 50)
print("📊 TEST SUMMARY")
print("=" * 50)
print(f"Tests Run: {self.tests_run}")
print(f"Tests Passed: {self.tests_passed}")
print(f"Tests Failed: {self.tests_run - self.tests_passed}")
print(f"Success Rate: {(self.tests_passed / self.tests_run * 100):.1f}%" if self.tests_run > 0 else "0%")
failed_tests = [test for test in self.test_results if test['status'] == 'FAILED']
if failed_tests:
print("\n❌ FAILED TESTS:")
for test in failed_tests:
print(f" - {test['test']}: {test['message']}")
print("\n" + "=" * 50)
return {
"total_tests": self.tests_run,
"passed_tests": self.tests_passed,
"failed_tests": self.tests_run - self.tests_passed,
"success_rate": (self.tests_passed / self.tests_run * 100) if self.tests_run > 0 else 0,
"detailed_results": self.test_results
}
def main():
tester = EpicTravelAPITester()
results = tester.run_all_tests()
# Return appropriate exit code
return 0 if results['failed_tests'] == 0 else 1
if __name__ == "__main__":
sys.exit(main())
+123 -66
View File
@@ -13,12 +13,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
import { toast } from 'sonner';
import { destinations as initialDestinations, specials as initialSpecials } from '../mockData';
import { destinationsAPI, specialsAPI, uploadAPI } from '../services/api';
const AdminDashboard = () => {
const navigate = useNavigate();
const [destinations, setDestinations] = useState(initialDestinations);
const [specials, setSpecials] = useState(initialSpecials);
const [destinations, setDestinations] = useState([]);
const [specials, setSpecials] = useState([]);
const [loading, setLoading] = useState(true);
const [isEditMode, setIsEditMode] = useState(false);
const [editingDestination, setEditingDestination] = useState(null);
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
@@ -37,35 +38,59 @@ const AdminDashboard = () => {
const isAuthenticated = localStorage.getItem('isAdminAuthenticated');
if (!isAuthenticated) {
navigate('/admin');
} else {
fetchData();
}
}, [navigate]);
const fetchData = async () => {
try {
setLoading(true);
const [destinationsData, specialsData] = await Promise.all([
destinationsAPI.getAll(),
specialsAPI.getAll()
]);
setDestinations(destinationsData);
setSpecials(specialsData);
} catch (error) {
console.error('Error fetching data:', error);
toast.error('Failed to load data');
} finally {
setLoading(false);
}
};
const handleLogout = () => {
localStorage.removeItem('isAdminAuthenticated');
localStorage.removeItem('auth_token');
toast.success('Logged out successfully');
navigate('/admin');
};
const handleAddDestination = () => {
const newDest = {
...newDestination,
id: String(destinations.length + 1),
rating: parseFloat(newDestination.rating),
price: parseFloat(newDestination.price)
};
setDestinations([...destinations, newDest]);
setIsAddDialogOpen(false);
setNewDestination({
name: '',
location: '',
description: '',
image: '',
category: 'City',
rating: 4.5,
price: 999,
currency: 'USD'
});
toast.success('Destination added successfully!');
const handleAddDestination = async () => {
try {
const newDest = await destinationsAPI.create({
...newDestination,
rating: parseFloat(newDestination.rating),
price: parseFloat(newDestination.price)
});
setDestinations([...destinations, newDest]);
setIsAddDialogOpen(false);
setNewDestination({
name: '',
location: '',
description: '',
image: '',
category: 'City',
rating: 4.5,
price: 999,
currency: 'USD'
});
toast.success('Destination added successfully!');
} catch (error) {
console.error('Error adding destination:', error);
toast.error('Failed to add destination');
}
};
const handleEditDestination = (destination) => {
@@ -73,45 +98,71 @@ const AdminDashboard = () => {
setIsEditMode(true);
};
const handleSaveEdit = () => {
setDestinations(destinations.map(dest =>
dest.id === editingDestination.id ? editingDestination : dest
));
setIsEditMode(false);
setEditingDestination(null);
toast.success('Destination updated successfully!');
};
const handleDeleteDestination = (id) => {
setDestinations(destinations.filter(dest => dest.id !== id));
// Also remove from specials if exists
setSpecials(specials.filter(special => special.destinationId !== id));
toast.success('Destination deleted successfully!');
};
const handleToggleSpecial = (destinationId) => {
const existingSpecial = specials.find(s => s.destinationId === destinationId);
if (existingSpecial) {
setSpecials(specials.filter(s => s.destinationId !== destinationId));
toast.success('Removed from specials');
} else {
const newSpecial = {
destinationId,
discount: 20,
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
highlights: ['Special offer', 'Limited time', 'Book now']
};
setSpecials([...specials, newSpecial]);
toast.success('Added to specials!');
const handleSaveEdit = async () => {
try {
const updated = await destinationsAPI.update(editingDestination.id, editingDestination);
setDestinations(destinations.map(dest =>
dest.id === updated.id ? updated : dest
));
setIsEditMode(false);
setEditingDestination(null);
toast.success('Destination updated successfully!');
} catch (error) {
console.error('Error updating destination:', error);
toast.error('Failed to update destination');
}
};
const handleUpdateSpecial = (destinationId, field, value) => {
setSpecials(specials.map(special =>
special.destinationId === destinationId
? { ...special, [field]: field === 'discount' ? parseFloat(value) : value }
: special
));
const handleDeleteDestination = async (id) => {
try {
await destinationsAPI.delete(id);
setDestinations(destinations.filter(dest => dest.id !== id));
setSpecials(specials.filter(special => special.destination_id !== id));
toast.success('Destination deleted successfully!');
} catch (error) {
console.error('Error deleting destination:', error);
toast.error('Failed to delete destination');
}
};
const handleToggleSpecial = async (destinationId) => {
const existingSpecial = specials.find(s => s.destination_id === destinationId);
try {
if (existingSpecial) {
await specialsAPI.deleteByDestination(destinationId);
setSpecials(specials.filter(s => s.destination_id !== destinationId));
toast.success('Removed from specials');
} else {
const newSpecial = await specialsAPI.create({
destination_id: destinationId,
discount: 20,
end_date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
highlights: ['Special offer', 'Limited time', 'Book now']
});
setSpecials([...specials, newSpecial]);
toast.success('Added to specials!');
}
} catch (error) {
console.error('Error toggling special:', error);
toast.error('Failed to update special');
}
};
const handleUpdateSpecial = async (specialId, field, value) => {
try {
const updatedSpecial = specials.find(s => s.id === specialId);
if (!updatedSpecial) return;
const updateData = { [field]: field === 'discount' ? parseFloat(value) : value };
const updated = await specialsAPI.update(specialId, updateData);
setSpecials(specials.map(special =>
special.id === specialId ? updated : special
));
} catch (error) {
console.error('Error updating special:', error);
toast.error('Failed to update special');
}
};
return (
@@ -247,8 +298,13 @@ const AdminDashboard = () => {
</Dialog>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{destinations.map((destination) => (
{loading ? (
<div className="text-center py-12">
<p className="text-gray-600">Loading destinations...</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{destinations.map((destination) => (
<Card key={destination.id} className="overflow-hidden">
<div className="relative">
<img
@@ -279,7 +335,7 @@ const AdminDashboard = () => {
<p className="text-sm text-gray-600 mb-4 line-clamp-2">{destination.description}</p>
<div className="flex items-center justify-between mb-4">
<span className="text-xl font-bold text-cyan-600">${destination.price}</span>
{specials.some(s => s.destinationId === destination.id) && (
{specials.some(s => s.destination_id === destination.id) && (
<Badge variant="outline" className="border-red-500 text-red-500">Special</Badge>
)}
</div>
@@ -307,6 +363,7 @@ const AdminDashboard = () => {
</Card>
))}
</div>
)}
</TabsContent>
{/* Specials Management */}
@@ -318,7 +375,7 @@ const AdminDashboard = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{destinations.map((destination) => {
const special = specials.find(s => s.destinationId === destination.id);
const special = specials.find(s => s.destination_id === destination.id);
return (
<Card key={destination.id}>
<CardContent className="pt-6">
@@ -342,7 +399,7 @@ const AdminDashboard = () => {
<Input
type="number"
value={special.discount}
onChange={(e) => handleUpdateSpecial(destination.id, 'discount', e.target.value)}
onChange={(e) => handleUpdateSpecial(special.id, 'discount', e.target.value)}
className="h-8"
/>
</div>
@@ -350,8 +407,8 @@ const AdminDashboard = () => {
<label className="text-xs text-gray-600">End Date</label>
<Input
type="date"
value={special.endDate}
onChange={(e) => handleUpdateSpecial(destination.id, 'endDate', e.target.value)}
value={special.end_date}
onChange={(e) => handleUpdateSpecial(special.id, 'end_date', e.target.value)}
className="h-8"
/>
</div>
+13 -6
View File
@@ -5,22 +5,29 @@ import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
import { toast } from 'sonner';
import { authAPI } from '../services/api';
const AdminLogin = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = (e) => {
const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
// Mock authentication - will be replaced with real backend
if (email === 'admin@epictravel.com' && password === 'admin123') {
try {
const response = await authAPI.login(email, password);
localStorage.setItem('auth_token', response.access_token);
localStorage.setItem('isAdminAuthenticated', 'true');
toast.success('Login successful!');
navigate('/admin/dashboard');
} else {
} catch (error) {
console.error('Login error:', error);
toast.error('Invalid credentials. Try: admin@epictravel.com / admin123');
} finally {
setLoading(false);
}
};
@@ -68,8 +75,8 @@ const AdminLogin = () => {
/>
</div>
</div>
<Button type="submit" className="w-full bg-cyan-600 hover:bg-cyan-700" size="lg">
Sign In
<Button type="submit" className="w-full bg-cyan-600 hover:bg-cyan-700" size="lg" disabled={loading}>
{loading ? 'Signing in...' : 'Sign In'}
</Button>
</form>
<div className="mt-6 p-4 bg-cyan-50 rounded-lg border border-cyan-200">
+122 -63
View File
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { MapPin, Star, Calendar, Tag, Search, Send, Mail, Phone, MessageSquare } from 'lucide-react';
import { destinations, specials, testimonials, categories } from '../mockData';
import { testimonials, categories } from '../mockData';
import { destinationsAPI, specialsAPI, contactAPI, newsletterAPI } from '../services/api';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Textarea } from '../components/ui/textarea';
@@ -13,12 +14,44 @@ const Home = () => {
const [searchQuery, setSearchQuery] = useState('');
const [contactForm, setContactForm] = useState({ name: '', email: '', message: '' });
const [newsletterEmail, setNewsletterEmail] = useState('');
const [destinations, setDestinations] = useState([]);
const [specials, setSpecials] = useState([]);
const [loading, setLoading] = useState(true);
// Fetch destinations and specials on mount
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const [destinationsData, specialsData] = await Promise.all([
destinationsAPI.getAll(),
specialsAPI.getAll()
]);
setDestinations(destinationsData);
setSpecials(specialsData);
} catch (error) {
console.error('Error fetching data:', error);
toast.error('Failed to load destinations');
} finally {
setLoading(false);
}
};
// Get special destinations
const specialDestinations = specials.map(special => {
const dest = destinations.find(d => d.id === special.destinationId);
return { ...dest, ...special };
});
const dest = destinations.find(d => d.id === special.destination_id);
if (!dest) return null;
return {
...dest,
discount: special.discount,
endDate: special.end_date,
highlights: special.highlights,
specialId: special.id
};
}).filter(Boolean);
// Filter destinations
const filteredDestinations = destinations.filter(dest => {
@@ -28,16 +61,26 @@ const Home = () => {
return matchesCategory && matchesSearch;
});
const handleContactSubmit = (e) => {
const handleContactSubmit = async (e) => {
e.preventDefault();
toast.success('Message sent! We\'ll get back to you soon.');
setContactForm({ name: '', email: '', message: '' });
try {
await contactAPI.submit(contactForm);
toast.success('Message sent! We\'ll get back to you soon.');
setContactForm({ name: '', email: '', message: '' });
} catch (error) {
toast.error('Failed to send message. Please try again.');
}
};
const handleNewsletterSubmit = (e) => {
const handleNewsletterSubmit = async (e) => {
e.preventDefault();
toast.success('Successfully subscribed to our newsletter!');
setNewsletterEmail('');
try {
await newsletterAPI.subscribe(newsletterEmail);
toast.success('Successfully subscribed to our newsletter!');
setNewsletterEmail('');
} catch (error) {
toast.error('Failed to subscribe. Please try again.');
}
};
return (
@@ -95,59 +138,69 @@ const Home = () => {
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{specialDestinations.map((special) => (
<Card key={special.id} className="overflow-hidden hover:shadow-2xl transition-shadow duration-300 group">
<div className="relative overflow-hidden">
<img
src={special.image}
alt={special.name}
className="w-full h-64 object-cover group-hover:scale-110 transition-transform duration-500"
/>
<div className="absolute top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-full font-bold text-lg shadow-lg">
{special.discount}% OFF
{loading ? (
<div className="text-center py-12">
<p className="text-gray-600">Loading specials...</p>
</div>
) : specialDestinations.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-600">No special offers available at the moment.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{specialDestinations.map((special) => (
<Card key={special.id} className="overflow-hidden hover:shadow-2xl transition-shadow duration-300 group">
<div className="relative overflow-hidden">
<img
src={special.image}
alt={special.name}
className="w-full h-64 object-cover group-hover:scale-110 transition-transform duration-500"
/>
<div className="absolute top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-full font-bold text-lg shadow-lg">
{special.discount}% OFF
</div>
<div className="absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm px-3 py-1 rounded-full flex items-center space-x-1">
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
<span className="font-semibold">{special.rating}</span>
</div>
</div>
<div className="absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm px-3 py-1 rounded-full flex items-center space-x-1">
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
<span className="font-semibold">{special.rating}</span>
</div>
</div>
<CardHeader>
<CardTitle className="text-2xl">{special.name}</CardTitle>
<CardDescription className="flex items-center text-base">
<MapPin className="w-4 h-4 mr-1" />
{special.location}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-gray-600 mb-4">{special.description}</p>
<div className="space-y-2 mb-4">
{special.highlights.map((highlight, idx) => (
<div key={idx} className="flex items-start space-x-2 text-sm text-gray-700">
<Tag className="w-4 h-4 text-cyan-600 mt-0.5 flex-shrink-0" />
<span>{highlight}</span>
<CardHeader>
<CardTitle className="text-2xl">{special.name}</CardTitle>
<CardDescription className="flex items-center text-base">
<MapPin className="w-4 h-4 mr-1" />
{special.location}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-gray-600 mb-4">{special.description}</p>
<div className="space-y-2 mb-4">
{special.highlights.map((highlight, idx) => (
<div key={idx} className="flex items-start space-x-2 text-sm text-gray-700">
<Tag className="w-4 h-4 text-cyan-600 mt-0.5 flex-shrink-0" />
<span>{highlight}</span>
</div>
))}
</div>
<div className="flex items-center justify-between mb-4">
<div>
<span className="text-gray-500 line-through text-lg">${special.price}</span>
<span className="text-3xl font-bold text-cyan-600 ml-2">
${Math.round(special.price * (1 - special.discount / 100))}
</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="w-4 h-4 mr-1" />
Until {new Date(special.endDate).toLocaleDateString()}
</div>
))}
</div>
<div className="flex items-center justify-between mb-4">
<div>
<span className="text-gray-500 line-through text-lg">${special.price}</span>
<span className="text-3xl font-bold text-cyan-600 ml-2">
${Math.round(special.price * (1 - special.discount / 100))}
</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="w-4 h-4 mr-1" />
Until {new Date(special.endDate).toLocaleDateString()}
</div>
</div>
<Button className="w-full bg-cyan-600 hover:bg-cyan-700">
Book Now
</Button>
</CardContent>
</Card>
))}
</div>
<Button className="w-full bg-cyan-600 hover:bg-cyan-700">
Book Now
</Button>
</CardContent>
</Card>
))}
</div>
)}
</div>
</section>
@@ -190,8 +243,13 @@ const Home = () => {
</div>
{/* Destinations Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredDestinations.map((destination) => (
{loading ? (
<div className="text-center py-12">
<p className="text-gray-600">Loading destinations...</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredDestinations.map((destination) => (
<Card key={destination.id} className="overflow-hidden hover:shadow-xl transition-shadow duration-300 group cursor-pointer">
<div className="relative overflow-hidden">
<img
@@ -229,6 +287,7 @@ const Home = () => {
</Card>
))}
</div>
)}
</div>
</section>
+112
View File
@@ -0,0 +1,112 @@
import axios from 'axios';
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
const API = `${BACKEND_URL}/api`;
// Create axios instance
const apiClient = axios.create({
baseURL: API,
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token to requests if available
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Auth API
export const authAPI = {
login: async (email, password) => {
const response = await apiClient.post('/auth/login', { email, password });
return response.data;
},
verify: async () => {
const response = await apiClient.post('/auth/verify');
return response.data;
},
};
// Destinations API
export const destinationsAPI = {
getAll: async (category, search) => {
const params = {};
if (category) params.category = category;
if (search) params.search = search;
const response = await apiClient.get('/destinations', { params });
return response.data;
},
getById: async (id) => {
const response = await apiClient.get(`/destinations/${id}`);
return response.data;
},
create: async (destination) => {
const response = await apiClient.post('/destinations', destination);
return response.data;
},
update: async (id, destination) => {
const response = await apiClient.put(`/destinations/${id}`, destination);
return response.data;
},
delete: async (id) => {
const response = await apiClient.delete(`/destinations/${id}`);
return response.data;
},
};
// Specials API
export const specialsAPI = {
getAll: async () => {
const response = await apiClient.get('/specials');
return response.data;
},
create: async (special) => {
const response = await apiClient.post('/specials', special);
return response.data;
},
update: async (id, special) => {
const response = await apiClient.put(`/specials/${id}`, special);
return response.data;
},
deleteByDestination: async (destinationId) => {
const response = await apiClient.delete(`/specials/destination/${destinationId}`);
return response.data;
},
};
// Contact API
export const contactAPI = {
submit: async (contact) => {
const response = await apiClient.post('/contact', contact);
return response.data;
},
};
// Newsletter API
export const newsletterAPI = {
subscribe: async (email) => {
const response = await apiClient.post('/newsletter/subscribe', { email });
return response.data;
},
};
// Image Upload API
export const uploadAPI = {
uploadImage: async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post('/upload/image', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
},
};
export default apiClient;