Subtotal: {invoice.get('subtotal', 0):.2f}
{f'Discount: -{invoice.get("discount_amount", 0):.2f}
' if invoice.get('discount_amount', 0) > 0 else ''} + {f'Promotion Code: {invoice.get("promotion_code", "")}
' if invoice.get('promotion_code') else ''}Tax: {invoice.get('tax_amount', 0):.2f}
Total Amount: {invoice.get('total_amount', 0):.2f}
Amount Paid: {invoice.get('amount_paid', 0):.2f}
@@ -729,19 +737,31 @@ async def create_booking( # Get discount from booking booking_discount = float(booking.discount_amount) if booking.discount_amount else 0.0 + # Add promotion code to invoice notes if present + invoice_notes = invoice_kwargs.get("notes", "") + if booking.promotion_code: + promotion_note = f"Promotion Code: {booking.promotion_code}" + invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note + invoice_kwargs["notes"] = invoice_notes + # Create invoices based on payment method if payment_method == "cash": # For cash bookings: create invoice for 20% deposit + proforma for 80% remaining deposit_amount = float(total_price) * 0.2 remaining_amount = float(total_price) * 0.8 + # Calculate proportional discount for partial invoices + # Deposit invoice gets 20% of the discount, proforma gets 80% + deposit_discount = booking_discount * 0.2 if booking_discount > 0 else 0.0 + proforma_discount = booking_discount * 0.8 if booking_discount > 0 else 0.0 + # Create invoice for deposit (20%) deposit_invoice = InvoiceService.create_invoice_from_booking( booking_id=booking.id, db=db, created_by_id=current_user.id, tax_rate=tax_rate, - discount_amount=booking_discount, + discount_amount=deposit_discount, due_days=30, is_proforma=False, invoice_amount=deposit_amount, @@ -754,7 +774,7 @@ async def create_booking( db=db, created_by_id=current_user.id, tax_rate=tax_rate, - discount_amount=booking_discount, + discount_amount=proforma_discount, due_days=30, is_proforma=True, invoice_amount=remaining_amount, diff --git a/Backend/src/routes/contact_content_routes.py b/Backend/src/routes/contact_content_routes.py new file mode 100644 index 00000000..b239df30 --- /dev/null +++ b/Backend/src/routes/contact_content_routes.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json + +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType + +logger = get_logger(__name__) + +router = APIRouter(prefix="/contact-content", tags=["contact-content"]) + + +def serialize_page_content(content: PageContent) -> dict: + """Serialize PageContent model to dictionary""" + return { + "id": content.id, + "page_type": content.page_type.value, + "title": content.title, + "subtitle": content.subtitle, + "description": content.description, + "content": content.content, + "meta_title": content.meta_title, + "meta_description": content.meta_description, + "meta_keywords": content.meta_keywords, + "og_title": content.og_title, + "og_description": content.og_description, + "og_image": content.og_image, + "canonical_url": content.canonical_url, + "contact_info": json.loads(content.contact_info) if content.contact_info else None, + "map_url": content.map_url, + "is_active": content.is_active, + "created_at": content.created_at.isoformat() if content.created_at else None, + "updated_at": content.updated_at.isoformat() if content.updated_at else None, + } + + +@router.get("/") +async def get_contact_content( + db: Session = Depends(get_db) +): + """Get contact page content""" + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.CONTACT).first() + + if not content: + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = serialize_page_content(content) + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + logger.error(f"Error fetching contact content: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching contact content: {str(e)}" + ) + diff --git a/Backend/src/routes/footer_routes.py b/Backend/src/routes/footer_routes.py new file mode 100644 index 00000000..ba577829 --- /dev/null +++ b/Backend/src/routes/footer_routes.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json + +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType + +logger = get_logger(__name__) + +router = APIRouter(prefix="/footer", tags=["footer"]) + + +def serialize_page_content(content: PageContent) -> dict: + """Serialize PageContent model to dictionary""" + return { + "id": content.id, + "page_type": content.page_type.value, + "title": content.title, + "subtitle": content.subtitle, + "description": content.description, + "content": content.content, + "social_links": json.loads(content.social_links) if content.social_links else None, + "footer_links": json.loads(content.footer_links) if content.footer_links else None, + "badges": json.loads(content.badges) if content.badges else None, + "copyright_text": content.copyright_text, + "is_active": content.is_active, + "created_at": content.created_at.isoformat() if content.created_at else None, + "updated_at": content.updated_at.isoformat() if content.updated_at else None, + } + + +@router.get("/") +async def get_footer_content( + db: Session = Depends(get_db) +): + """Get footer content""" + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first() + + if not content: + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = serialize_page_content(content) + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + logger.error(f"Error fetching footer content: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching footer content: {str(e)}" + ) + diff --git a/Backend/src/routes/home_routes.py b/Backend/src/routes/home_routes.py new file mode 100644 index 00000000..5163c81e --- /dev/null +++ b/Backend/src/routes/home_routes.py @@ -0,0 +1,110 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import json + +from ..config.database import get_db +from ..config.logging_config import get_logger +from ..models.page_content import PageContent, PageType + +logger = get_logger(__name__) + +router = APIRouter(prefix="/home", tags=["home"]) + + +def serialize_page_content(content: PageContent) -> dict: + """Serialize PageContent model to dictionary""" + return { + "id": content.id, + "page_type": content.page_type.value, + "title": content.title, + "subtitle": content.subtitle, + "description": content.description, + "content": content.content, + "meta_title": content.meta_title, + "meta_description": content.meta_description, + "meta_keywords": content.meta_keywords, + "og_title": content.og_title, + "og_description": content.og_description, + "og_image": content.og_image, + "canonical_url": content.canonical_url, + "hero_title": content.hero_title, + "hero_subtitle": content.hero_subtitle, + "hero_image": content.hero_image, + "amenities_section_title": content.amenities_section_title, + "amenities_section_subtitle": content.amenities_section_subtitle, + "amenities": json.loads(content.amenities) if content.amenities else None, + "testimonials_section_title": content.testimonials_section_title, + "testimonials_section_subtitle": content.testimonials_section_subtitle, + "testimonials": json.loads(content.testimonials) if content.testimonials else None, + "gallery_section_title": content.gallery_section_title, + "gallery_section_subtitle": content.gallery_section_subtitle, + "gallery_images": json.loads(content.gallery_images) if content.gallery_images else None, + "luxury_section_title": content.luxury_section_title, + "luxury_section_subtitle": content.luxury_section_subtitle, + "luxury_section_image": content.luxury_section_image, + "luxury_features": json.loads(content.luxury_features) if content.luxury_features else None, + "luxury_gallery_section_title": content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None, + "luxury_testimonials_section_title": content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, + "about_preview_title": content.about_preview_title, + "about_preview_subtitle": content.about_preview_subtitle, + "about_preview_content": content.about_preview_content, + "about_preview_image": content.about_preview_image, + "stats": json.loads(content.stats) if content.stats else None, + "luxury_services_section_title": content.luxury_services_section_title, + "luxury_services_section_subtitle": content.luxury_services_section_subtitle, + "luxury_services": json.loads(content.luxury_services) if content.luxury_services else None, + "luxury_experiences_section_title": content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None, + "awards_section_title": content.awards_section_title, + "awards_section_subtitle": content.awards_section_subtitle, + "awards": json.loads(content.awards) if content.awards else None, + "cta_title": content.cta_title, + "cta_subtitle": content.cta_subtitle, + "cta_button_text": content.cta_button_text, + "cta_button_link": content.cta_button_link, + "cta_image": content.cta_image, + "partners_section_title": content.partners_section_title, + "partners_section_subtitle": content.partners_section_subtitle, + "partners": json.loads(content.partners) if content.partners else None, + "is_active": content.is_active, + "created_at": content.created_at.isoformat() if content.created_at else None, + "updated_at": content.updated_at.isoformat() if content.updated_at else None, + } + + +@router.get("/") +async def get_home_content( + db: Session = Depends(get_db) +): + """Get homepage content""" + try: + content = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first() + + if not content: + return { + "status": "success", + "data": { + "page_content": None + } + } + + content_dict = serialize_page_content(content) + + return { + "status": "success", + "data": { + "page_content": content_dict + } + } + except Exception as e: + logger.error(f"Error fetching home content: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching home content: {str(e)}" + ) + diff --git a/Backend/src/routes/invoice_routes.py b/Backend/src/routes/invoice_routes.py index 72a6be88..c121b123 100644 --- a/Backend/src/routes/invoice_routes.py +++ b/Backend/src/routes/invoice_routes.py @@ -87,11 +87,38 @@ async def create_invoice( if not booking_id: raise HTTPException(status_code=400, detail="booking_id is required") + # Ensure booking_id is an integer + try: + booking_id = int(booking_id) + except (ValueError, TypeError): + raise HTTPException(status_code=400, detail="booking_id must be a valid integer") + # Check if booking exists booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(status_code=404, detail="Booking not found") + # Prepare invoice kwargs + invoice_kwargs = { + "company_name": invoice_data.get("company_name"), + "company_address": invoice_data.get("company_address"), + "company_phone": invoice_data.get("company_phone"), + "company_email": invoice_data.get("company_email"), + "company_tax_id": invoice_data.get("company_tax_id"), + "company_logo_url": invoice_data.get("company_logo_url"), + "customer_tax_id": invoice_data.get("customer_tax_id"), + "notes": invoice_data.get("notes"), + "terms_and_conditions": invoice_data.get("terms_and_conditions"), + "payment_instructions": invoice_data.get("payment_instructions"), + } + + # Add promotion code to invoice notes if present in booking + invoice_notes = invoice_kwargs.get("notes", "") + if booking.promotion_code: + promotion_note = f"Promotion Code: {booking.promotion_code}" + invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note + invoice_kwargs["notes"] = invoice_notes + # Create invoice invoice = InvoiceService.create_invoice_from_booking( booking_id=booking_id, @@ -100,16 +127,7 @@ async def create_invoice( tax_rate=invoice_data.get("tax_rate", 0.0), discount_amount=invoice_data.get("discount_amount", 0.0), due_days=invoice_data.get("due_days", 30), - company_name=invoice_data.get("company_name"), - company_address=invoice_data.get("company_address"), - company_phone=invoice_data.get("company_phone"), - company_email=invoice_data.get("company_email"), - company_tax_id=invoice_data.get("company_tax_id"), - company_logo_url=invoice_data.get("company_logo_url"), - customer_tax_id=invoice_data.get("customer_tax_id"), - notes=invoice_data.get("notes"), - terms_and_conditions=invoice_data.get("terms_and_conditions"), - payment_instructions=invoice_data.get("payment_instructions"), + **invoice_kwargs ) return { diff --git a/Backend/src/routes/page_content_routes.py b/Backend/src/routes/page_content_routes.py index 05e1291f..bebef6eb 100644 --- a/Backend/src/routes/page_content_routes.py +++ b/Backend/src/routes/page_content_routes.py @@ -1,14 +1,21 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Request, UploadFile, File from sqlalchemy.orm import Session from typing import Optional from datetime import datetime +from pathlib import Path import json +import os +import aiofiles +import uuid from ..config.database import get_db +from ..config.logging_config import get_logger from ..middleware.auth import get_current_user, authorize_roles from ..models.user import User from ..models.page_content import PageContent, PageType +logger = get_logger(__name__) + router = APIRouter(prefix="/page-content", tags=["page-content"]) @@ -40,12 +47,60 @@ async def get_all_page_contents( "social_links": json.loads(content.social_links) if content.social_links else None, "footer_links": json.loads(content.footer_links) if content.footer_links else None, "badges": json.loads(content.badges) if content.badges else None, + "copyright_text": content.copyright_text, "hero_title": content.hero_title, "hero_subtitle": content.hero_subtitle, "hero_image": content.hero_image, "story_content": content.story_content, "values": json.loads(content.values) if content.values else None, "features": json.loads(content.features) if content.features else None, + "about_hero_image": content.about_hero_image, + "mission": content.mission, + "vision": content.vision, + "team": json.loads(content.team) if content.team else None, + "timeline": json.loads(content.timeline) if content.timeline else None, + "achievements": json.loads(content.achievements) if content.achievements else None, + "amenities_section_title": content.amenities_section_title, + "amenities_section_subtitle": content.amenities_section_subtitle, + "amenities": json.loads(content.amenities) if content.amenities else None, + "testimonials_section_title": content.testimonials_section_title, + "testimonials_section_subtitle": content.testimonials_section_subtitle, + "testimonials": json.loads(content.testimonials) if content.testimonials else None, + "gallery_section_title": content.gallery_section_title, + "gallery_section_subtitle": content.gallery_section_subtitle, + "gallery_images": json.loads(content.gallery_images) if content.gallery_images else None, + "luxury_section_title": content.luxury_section_title, + "luxury_section_subtitle": content.luxury_section_subtitle, + "luxury_section_image": content.luxury_section_image, + "luxury_features": json.loads(content.luxury_features) if content.luxury_features else None, + "luxury_gallery_section_title": content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None, + "luxury_testimonials_section_title": content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, + "about_preview_title": content.about_preview_title, + "about_preview_subtitle": content.about_preview_subtitle, + "about_preview_content": content.about_preview_content, + "about_preview_image": content.about_preview_image, + "stats": json.loads(content.stats) if content.stats else None, + "luxury_services_section_title": content.luxury_services_section_title, + "luxury_services_section_subtitle": content.luxury_services_section_subtitle, + "luxury_services": json.loads(content.luxury_services) if content.luxury_services else None, + "luxury_experiences_section_title": content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None, + "awards_section_title": content.awards_section_title, + "awards_section_subtitle": content.awards_section_subtitle, + "awards": json.loads(content.awards) if content.awards else None, + "cta_title": content.cta_title, + "cta_subtitle": content.cta_subtitle, + "cta_button_text": content.cta_button_text, + "cta_button_link": content.cta_button_link, + "cta_image": content.cta_image, + "partners_section_title": content.partners_section_title, + "partners_section_subtitle": content.partners_section_subtitle, + "partners": json.loads(content.partners) if content.partners else None, "is_active": content.is_active, "created_at": content.created_at.isoformat() if content.created_at else None, "updated_at": content.updated_at.isoformat() if content.updated_at else None, @@ -65,6 +120,104 @@ async def get_all_page_contents( ) +def get_base_url(request: Request) -> str: + """Get base URL for image normalization""" + return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:8000')}" + + +def normalize_image_url(image_url: str, base_url: str) -> str: + """Normalize image URL to absolute URL""" + if not image_url: + return image_url + if image_url.startswith('http://') or image_url.startswith('https://'): + return image_url + if image_url.startswith('/'): + return f"{base_url}{image_url}" + return f"{base_url}/{image_url}" + + +@router.post("/upload", dependencies=[Depends(authorize_roles("admin"))]) +async def upload_page_content_image( + request: Request, + image: UploadFile = File(...), + current_user: User = Depends(authorize_roles("admin")), +): + """Upload page content image (Admin only)""" + try: + logger.info(f"Upload request received: filename={image.filename}, content_type={image.content_type}") + + # Validate file exists + if not image: + logger.error("No file provided in upload request") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No file provided" + ) + + # Validate file type + if not image.content_type or not image.content_type.startswith('image/'): + logger.error(f"Invalid file type: {image.content_type}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File must be an image. Received: {image.content_type}" + ) + + # Validate filename + if not image.filename: + logger.error("No filename provided in upload request") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Filename is required" + ) + + # Create uploads directory + upload_dir = Path(__file__).parent.parent.parent / "uploads" / "page-content" + upload_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"Upload directory: {upload_dir}") + + # Generate filename + ext = Path(image.filename).suffix or '.jpg' + filename = f"page-content-{uuid.uuid4()}{ext}" + file_path = upload_dir / filename + + # Save file + async with aiofiles.open(file_path, 'wb') as f: + content = await image.read() + if not content: + logger.error("Empty file uploaded") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File is empty" + ) + await f.write(content) + logger.info(f"File saved successfully: {file_path}, size: {len(content)} bytes") + + # Return the image URL + image_url = f"/uploads/page-content/{filename}" + base_url = get_base_url(request) + full_url = normalize_image_url(image_url, base_url) + + logger.info(f"Upload successful: {image_url}") + return { + "success": True, + "status": "success", + "message": "Image uploaded successfully", + "data": { + "image_url": image_url, + "full_url": full_url + } + } + except HTTPException as e: + logger.error(f"HTTPException in upload: {e.detail}") + raise + except Exception as e: + logger.error(f"Unexpected error uploading image: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error uploading image: {str(e)}" + ) + + @router.get("/{page_type}") async def get_page_content( page_type: PageType, @@ -102,12 +255,60 @@ async def get_page_content( "social_links": json.loads(content.social_links) if content.social_links else None, "footer_links": json.loads(content.footer_links) if content.footer_links else None, "badges": json.loads(content.badges) if content.badges else None, + "copyright_text": content.copyright_text, "hero_title": content.hero_title, "hero_subtitle": content.hero_subtitle, "hero_image": content.hero_image, "story_content": content.story_content, "values": json.loads(content.values) if content.values else None, "features": json.loads(content.features) if content.features else None, + "about_hero_image": content.about_hero_image, + "mission": content.mission, + "vision": content.vision, + "team": json.loads(content.team) if content.team else None, + "timeline": json.loads(content.timeline) if content.timeline else None, + "achievements": json.loads(content.achievements) if content.achievements else None, + "amenities_section_title": content.amenities_section_title, + "amenities_section_subtitle": content.amenities_section_subtitle, + "amenities": json.loads(content.amenities) if content.amenities else None, + "testimonials_section_title": content.testimonials_section_title, + "testimonials_section_subtitle": content.testimonials_section_subtitle, + "testimonials": json.loads(content.testimonials) if content.testimonials else None, + "gallery_section_title": content.gallery_section_title, + "gallery_section_subtitle": content.gallery_section_subtitle, + "gallery_images": json.loads(content.gallery_images) if content.gallery_images else None, + "luxury_section_title": content.luxury_section_title, + "luxury_section_subtitle": content.luxury_section_subtitle, + "luxury_section_image": content.luxury_section_image, + "luxury_features": json.loads(content.luxury_features) if content.luxury_features else None, + "luxury_gallery_section_title": content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None, + "luxury_testimonials_section_title": content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, + "about_preview_title": content.about_preview_title, + "about_preview_subtitle": content.about_preview_subtitle, + "about_preview_content": content.about_preview_content, + "about_preview_image": content.about_preview_image, + "stats": json.loads(content.stats) if content.stats else None, + "luxury_services_section_title": content.luxury_services_section_title, + "luxury_services_section_subtitle": content.luxury_services_section_subtitle, + "luxury_services": json.loads(content.luxury_services) if content.luxury_services else None, + "luxury_experiences_section_title": content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None, + "awards_section_title": content.awards_section_title, + "awards_section_subtitle": content.awards_section_subtitle, + "awards": json.loads(content.awards) if content.awards else None, + "cta_title": content.cta_title, + "cta_subtitle": content.cta_subtitle, + "cta_button_text": content.cta_button_text, + "cta_button_link": content.cta_button_link, + "cta_image": content.cta_image, + "partners_section_title": content.partners_section_title, + "partners_section_subtitle": content.partners_section_subtitle, + "partners": json.loads(content.partners) if content.partners else None, "is_active": content.is_active, "created_at": content.created_at.isoformat() if content.created_at else None, "updated_at": content.updated_at.isoformat() if content.updated_at else None, @@ -151,6 +352,12 @@ async def create_or_update_page_content( story_content: Optional[str] = None, values: Optional[str] = None, features: Optional[str] = None, + about_hero_image: Optional[str] = None, + mission: Optional[str] = None, + vision: Optional[str] = None, + team: Optional[str] = None, + timeline: Optional[str] = None, + achievements: Optional[str] = None, is_active: bool = True, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) @@ -263,6 +470,18 @@ async def create_or_update_page_content( existing_content.values = values if features is not None: existing_content.features = features + if about_hero_image is not None: + existing_content.about_hero_image = about_hero_image + if mission is not None: + existing_content.mission = mission + if vision is not None: + existing_content.vision = vision + if team is not None: + existing_content.team = team + if timeline is not None: + existing_content.timeline = timeline + if achievements is not None: + existing_content.achievements = achievements if is_active is not None: existing_content.is_active = is_active @@ -308,6 +527,12 @@ async def create_or_update_page_content( story_content=story_content, values=values, features=features, + about_hero_image=about_hero_image, + mission=mission, + vision=vision, + team=team, + timeline=timeline, + achievements=achievements, is_active=is_active, ) @@ -362,7 +587,10 @@ async def update_page_content( for key, value in page_data.items(): if hasattr(existing_content, key): # Handle JSON fields - convert dict/list to JSON string - if key in ["contact_info", "social_links", "footer_links", "badges", "values", "features"] and value is not None: + if key in ["contact_info", "social_links", "footer_links", "badges", "values", "features", + "amenities", "testimonials", "gallery_images", "stats", "luxury_features", + "luxury_gallery", "luxury_testimonials", "luxury_services", "luxury_experiences", + "awards", "partners", "team", "timeline", "achievements"] and value is not None: if isinstance(value, str): # Already a string, validate it's valid JSON try: @@ -403,12 +631,60 @@ async def update_page_content( "social_links": json.loads(existing_content.social_links) if existing_content.social_links else None, "footer_links": json.loads(existing_content.footer_links) if existing_content.footer_links else None, "badges": json.loads(existing_content.badges) if existing_content.badges else None, + "copyright_text": existing_content.copyright_text, "hero_title": existing_content.hero_title, "hero_subtitle": existing_content.hero_subtitle, "hero_image": existing_content.hero_image, "story_content": existing_content.story_content, "values": json.loads(existing_content.values) if existing_content.values else None, "features": json.loads(existing_content.features) if existing_content.features else None, + "about_hero_image": existing_content.about_hero_image, + "mission": existing_content.mission, + "vision": existing_content.vision, + "team": json.loads(existing_content.team) if existing_content.team else None, + "timeline": json.loads(existing_content.timeline) if existing_content.timeline else None, + "achievements": json.loads(existing_content.achievements) if existing_content.achievements else None, + "amenities_section_title": existing_content.amenities_section_title, + "amenities_section_subtitle": existing_content.amenities_section_subtitle, + "amenities": json.loads(existing_content.amenities) if existing_content.amenities else None, + "testimonials_section_title": existing_content.testimonials_section_title, + "testimonials_section_subtitle": existing_content.testimonials_section_subtitle, + "testimonials": json.loads(existing_content.testimonials) if existing_content.testimonials else None, + "gallery_section_title": existing_content.gallery_section_title, + "gallery_section_subtitle": existing_content.gallery_section_subtitle, + "gallery_images": json.loads(existing_content.gallery_images) if existing_content.gallery_images else None, + "luxury_section_title": existing_content.luxury_section_title, + "luxury_section_subtitle": existing_content.luxury_section_subtitle, + "luxury_section_image": existing_content.luxury_section_image, + "luxury_features": json.loads(existing_content.luxury_features) if existing_content.luxury_features else None, + "luxury_gallery_section_title": existing_content.luxury_gallery_section_title, + "luxury_gallery_section_subtitle": existing_content.luxury_gallery_section_subtitle, + "luxury_gallery": json.loads(existing_content.luxury_gallery) if existing_content.luxury_gallery else None, + "luxury_testimonials_section_title": existing_content.luxury_testimonials_section_title, + "luxury_testimonials_section_subtitle": existing_content.luxury_testimonials_section_subtitle, + "luxury_testimonials": json.loads(existing_content.luxury_testimonials) if existing_content.luxury_testimonials else None, + "about_preview_title": existing_content.about_preview_title, + "about_preview_subtitle": existing_content.about_preview_subtitle, + "about_preview_content": existing_content.about_preview_content, + "about_preview_image": existing_content.about_preview_image, + "stats": json.loads(existing_content.stats) if existing_content.stats else None, + "luxury_services_section_title": existing_content.luxury_services_section_title, + "luxury_services_section_subtitle": existing_content.luxury_services_section_subtitle, + "luxury_services": json.loads(existing_content.luxury_services) if existing_content.luxury_services else None, + "luxury_experiences_section_title": existing_content.luxury_experiences_section_title, + "luxury_experiences_section_subtitle": existing_content.luxury_experiences_section_subtitle, + "luxury_experiences": json.loads(existing_content.luxury_experiences) if existing_content.luxury_experiences else None, + "awards_section_title": existing_content.awards_section_title, + "awards_section_subtitle": existing_content.awards_section_subtitle, + "awards": json.loads(existing_content.awards) if existing_content.awards else None, + "cta_title": existing_content.cta_title, + "cta_subtitle": existing_content.cta_subtitle, + "cta_button_text": existing_content.cta_button_text, + "cta_button_link": existing_content.cta_button_link, + "cta_image": existing_content.cta_image, + "partners_section_title": existing_content.partners_section_title, + "partners_section_subtitle": existing_content.partners_section_subtitle, + "partners": json.loads(existing_content.partners) if existing_content.partners else None, "is_active": existing_content.is_active, "updated_at": existing_content.updated_at.isoformat() if existing_content.updated_at else None, } diff --git a/Backend/src/routes/room_routes.py b/Backend/src/routes/room_routes.py index 7b07d220..d8a93732 100644 --- a/Backend/src/routes/room_routes.py +++ b/Backend/src/routes/room_routes.py @@ -694,18 +694,24 @@ async def upload_room_images( image_urls = [] for image in images: # Validate file type - if not image.content_type.startswith('image/'): + if not image.content_type or not image.content_type.startswith('image/'): + continue + + # Validate filename + if not image.filename: continue # Generate filename import uuid - ext = Path(image.filename).suffix + ext = Path(image.filename).suffix or '.jpg' filename = f"room-{uuid.uuid4()}{ext}" file_path = upload_dir / filename # Save file async with aiofiles.open(file_path, 'wb') as f: content = await image.read() + if not content: + continue await f.write(content) image_urls.append(f"/uploads/rooms/{filename}") @@ -717,6 +723,7 @@ async def upload_room_images( db.commit() return { + "success": True, "status": "success", "message": "Images uploaded successfully", "data": {"images": updated_images} diff --git a/Backend/src/routes/system_settings_routes.py b/Backend/src/routes/system_settings_routes.py index 2ee87661..bb87387b 100644 --- a/Backend/src/routes/system_settings_routes.py +++ b/Backend/src/routes/system_settings_routes.py @@ -1127,6 +1127,7 @@ async def upload_company_logo( full_url = normalize_image_url(image_url, base_url) return { + "success": True, "status": "success", "message": "Logo uploaded successfully", "data": { @@ -1235,6 +1236,7 @@ async def upload_company_favicon( full_url = normalize_image_url(image_url, base_url) return { + "success": True, "status": "success", "message": "Favicon uploaded successfully", "data": { diff --git a/Backend/src/services/__pycache__/auth_service.cpython-312.pyc b/Backend/src/services/__pycache__/auth_service.cpython-312.pyc index c4839a65..723a4c40 100644 Binary files a/Backend/src/services/__pycache__/auth_service.cpython-312.pyc and b/Backend/src/services/__pycache__/auth_service.cpython-312.pyc differ diff --git a/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc b/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc index cc230db6..f1042965 100644 Binary files a/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc and b/Backend/src/services/__pycache__/invoice_service.cpython-312.pyc differ diff --git a/Backend/src/services/__pycache__/paypal_service.cpython-312.pyc b/Backend/src/services/__pycache__/paypal_service.cpython-312.pyc index d222bf0b..9d2b7f13 100644 Binary files a/Backend/src/services/__pycache__/paypal_service.cpython-312.pyc and b/Backend/src/services/__pycache__/paypal_service.cpython-312.pyc differ diff --git a/Backend/src/services/__pycache__/stripe_service.cpython-312.pyc b/Backend/src/services/__pycache__/stripe_service.cpython-312.pyc index 015ef5b6..f1cf0ccc 100644 Binary files a/Backend/src/services/__pycache__/stripe_service.cpython-312.pyc and b/Backend/src/services/__pycache__/stripe_service.cpython-312.pyc differ diff --git a/Backend/src/services/auth_service.py b/Backend/src/services/auth_service.py index 5fa6b266..f8fc7cf0 100644 --- a/Backend/src/services/auth_service.py +++ b/Backend/src/services/auth_service.py @@ -146,16 +146,28 @@ class AuthService: async def login(self, db: Session, email: str, password: str, remember_me: bool = False, mfa_token: str = None) -> dict: """Login user with optional MFA verification""" + # Normalize email (lowercase and strip whitespace) + email = email.lower().strip() if email else "" + if not email: + raise ValueError("Invalid email or password") + # Find user with role and password user = db.query(User).filter(User.email == email).first() if not user: + logger.warning(f"Login attempt with non-existent email: {email}") raise ValueError("Invalid email or password") + # Check if user is active + if not user.is_active: + logger.warning(f"Login attempt for inactive user: {email}") + raise ValueError("Account is disabled. Please contact support.") + # Load role user.role = db.query(Role).filter(Role.id == user.role_id).first() # Check password if not self.verify_password(password, user.password): + logger.warning(f"Login attempt with invalid password for user: {email}") raise ValueError("Invalid email or password") # Check if MFA is enabled diff --git a/Backend/src/services/invoice_service.py b/Backend/src/services/invoice_service.py index 00a192a8..73fe769e 100644 --- a/Backend/src/services/invoice_service.py +++ b/Backend/src/services/invoice_service.py @@ -87,10 +87,20 @@ class InvoiceService: # Calculate amounts - subtotal will be recalculated after adding items # Initial subtotal is booking total (room + services) or invoice_amount if specified + booking_total = float(booking.total_price) if invoice_amount is not None: subtotal = float(invoice_amount) + # For partial invoices, ensure discount is proportional + # If discount_amount seems too large (greater than subtotal), recalculate proportionally + if invoice_amount < booking_total and discount_amount > 0: + # Check if discount seems disproportionate (greater than 50% of subtotal suggests it's the full discount) + if discount_amount > subtotal * 0.5: + # Recalculate proportionally from booking's original discount + proportion = float(invoice_amount) / booking_total + original_discount = float(booking.discount_amount) if booking.discount_amount else discount_amount + discount_amount = original_discount * proportion else: - subtotal = float(booking.total_price) + subtotal = booking_total # Calculate tax and total amounts tax_amount = (subtotal - discount_amount) * (tax_rate / 100) @@ -388,6 +398,14 @@ class InvoiceService: @staticmethod def invoice_to_dict(invoice: Invoice) -> Dict[str, Any]: """Convert invoice model to dictionary""" + # Extract promotion code from notes if present + promotion_code = None + if invoice.notes and "Promotion Code:" in invoice.notes: + try: + promotion_code = invoice.notes.split("Promotion Code:")[1].split("\n")[0].strip() + except: + pass + return { "id": invoice.id, "invoice_number": invoice.invoice_number, @@ -419,6 +437,7 @@ class InvoiceService: "terms_and_conditions": invoice.terms_and_conditions, "payment_instructions": invoice.payment_instructions, "is_proforma": invoice.is_proforma if hasattr(invoice, 'is_proforma') else False, + "promotion_code": promotion_code, "items": [ { "id": item.id, diff --git a/Backend/src/services/paypal_service.py b/Backend/src/services/paypal_service.py index d613741b..24c62f79 100644 --- a/Backend/src/services/paypal_service.py +++ b/Backend/src/services/paypal_service.py @@ -505,10 +505,36 @@ class PayPalService: except Exception as email_error: logger.error(f"Failed to send booking confirmation email: {str(email_error)}") + # Send invoice email if payment is completed and invoice is now paid + from ..utils.mailer import send_email + from ..services.invoice_service import InvoiceService + from ..routes.booking_routes import _generate_invoice_email_html + + # Load user for email + from ..models.user import User + user = db.query(User).filter(User.id == booking.user_id).first() + + for invoice in invoices: + if invoice.status == InvoiceStatus.paid and invoice.balance_due <= 0: + try: + invoice_dict = InvoiceService.invoice_to_dict(invoice) + invoice_html = _generate_invoice_email_html(invoice_dict, is_proforma=invoice.is_proforma) + invoice_type = "Proforma Invoice" if invoice.is_proforma else "Invoice" + if user: + await send_email( + to=user.email, + subject=f"{invoice_type} {invoice.invoice_number} - Payment Confirmed", + html=invoice_html + ) + logger.info(f"{invoice_type} {invoice.invoice_number} sent to {user.email}") + except Exception as email_error: + logger.error(f"Failed to send invoice email: {str(email_error)}") + # Send invoice email if payment is completed and invoice is now paid from ..utils.mailer import send_email from ..services.invoice_service import InvoiceService from ..models.invoice import InvoiceStatus + from ..routes.booking_routes import _generate_invoice_email_html # Load user for email from ..models.user import User diff --git a/Backend/src/services/stripe_service.py b/Backend/src/services/stripe_service.py index b63fa304..fd94ea8e 100644 --- a/Backend/src/services/stripe_service.py +++ b/Backend/src/services/stripe_service.py @@ -404,6 +404,7 @@ class StripeService: # Send invoice email if payment is completed and invoice is now paid from ..utils.mailer import send_email from ..services.invoice_service import InvoiceService + from ..routes.booking_routes import _generate_invoice_email_html # Load user for email from ..models.user import User diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 90398ab3..cd843de1 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -58,6 +58,8 @@ const ResetPasswordPage = lazy(() => import('./pages/auth/ResetPasswordPage')); // Lazy load admin pages const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage')); +const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage')); +const PaymentManagementPage = lazy(() => import('./pages/admin/PaymentManagementPage')); const UserManagementPage = lazy(() => import('./pages/admin/UserManagementPage')); const PageContentDashboardPage = lazy(() => import('./pages/admin/PageContentDashboard')); const AnalyticsDashboardPage = lazy(() => import('./pages/admin/AnalyticsDashboardPage')); @@ -321,6 +323,18 @@ function App() { path="settings" element={+ Showing {filteredIcons.length} icons. Popular icons appear first. +
+No icons found matching "{searchQuery}"
+Try a different search term
++ {currentBanner.description} +
+ )} + + {/* Animated decorative line below title/description */} diff --git a/Frontend/src/data/luxuryContentSeed.ts b/Frontend/src/data/luxuryContentSeed.ts new file mode 100644 index 00000000..efcda53c --- /dev/null +++ b/Frontend/src/data/luxuryContentSeed.ts @@ -0,0 +1,65 @@ +// Seed data for luxury hotel content +export const luxuryContentSeed = { + home: { + luxury_section_title: 'Experience Unparalleled Luxury', + luxury_section_subtitle: 'Where elegance meets comfort in every detail', + luxury_section_image: '', + luxury_features: [ + { + icon: 'Sparkles', + title: 'Premium Amenities', + description: 'World-class facilities designed for your comfort and relaxation' + }, + { + icon: 'Crown', + title: 'Royal Service', + description: 'Dedicated concierge service available 24/7 for all your needs' + }, + { + icon: 'Award', + title: 'Award-Winning', + description: 'Recognized for excellence in hospitality and guest satisfaction' + }, + { + icon: 'Shield', + title: 'Secure & Private', + description: 'Your privacy and security are our top priorities' + }, + { + icon: 'Heart', + title: 'Personalized Care', + description: 'Tailored experiences crafted just for you' + }, + { + icon: 'Gem', + title: 'Luxury Design', + description: 'Elegantly designed spaces with attention to every detail' + } + ], + luxury_gallery: [], + luxury_testimonials: [ + { + name: 'Sarah Johnson', + title: 'Business Executive', + quote: 'An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.', + image: '' + }, + { + name: 'Michael Chen', + title: 'Travel Enthusiast', + quote: 'The epitome of luxury. Every moment was perfect, from check-in to check-out.', + image: '' + }, + { + name: 'Emma Williams', + title: 'Luxury Traveler', + quote: 'This hotel redefines what luxury means. I will definitely return.', + image: '' + } + ], + about_preview_title: 'About Our Luxury Hotel', + about_preview_content: 'Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience.', + about_preview_image: '' + } +}; + diff --git a/Frontend/src/pages/AboutPage.tsx b/Frontend/src/pages/AboutPage.tsx index 661ebae6..4e880692 100644 --- a/Frontend/src/pages/AboutPage.tsx +++ b/Frontend/src/pages/AboutPage.tsx @@ -1,16 +1,14 @@ import React, { useState, useEffect } from 'react'; import { Hotel, - Award, - Users, Heart, MapPin, Phone, Mail, - Star, - Shield, - Clock + Linkedin, + Twitter } from 'lucide-react'; +import * as LucideIcons from 'lucide-react'; import { Link } from 'react-router-dom'; import { pageContentService } from '../services/api'; import type { PageContent } from '../services/api/pageContentService'; @@ -23,7 +21,7 @@ const AboutPage: React.FC = () => { useEffect(() => { const fetchPageContent = async () => { try { - const response = await pageContentService.getPageContent('about'); + const response = await pageContentService.getAboutContent(); if (response.status === 'success' && response.data?.page_content) { setPageContent(response.data.page_content); @@ -58,22 +56,22 @@ const AboutPage: React.FC = () => { // Default values const defaultValues = [ { - icon: Heart, + icon: 'Heart', title: 'Passion', description: 'We are passionate about hospitality and dedicated to creating exceptional experiences for every guest.' }, { - icon: Award, + icon: 'Award', title: 'Excellence', description: 'We strive for excellence in every aspect of our service, from the smallest detail to the grandest gesture.' }, { - icon: Shield, + icon: 'Shield', title: 'Integrity', description: 'We conduct our business with honesty, transparency, and respect for our guests and community.' }, { - icon: Users, + icon: 'Users', title: 'Service', description: 'Our guests are at the heart of everything we do. Your comfort and satisfaction are our top priorities.' } @@ -81,17 +79,17 @@ const AboutPage: React.FC = () => { const defaultFeatures = [ { - icon: Star, + icon: 'Star', title: 'Premium Accommodations', description: 'Luxuriously appointed rooms and suites designed for ultimate comfort and relaxation.' }, { - icon: Clock, + icon: 'Clock', title: '24/7 Service', description: 'Round-the-clock concierge and room service to attend to your needs at any time.' }, { - icon: Award, + icon: 'Award', title: 'Award-Winning', description: 'Recognized for excellence in hospitality and guest satisfaction.' } @@ -99,7 +97,7 @@ const AboutPage: React.FC = () => { const values = pageContent?.values && pageContent.values.length > 0 ? pageContent.values.map((v: any) => ({ - icon: defaultValues.find(d => d.title === v.title)?.icon || Heart, + icon: v.icon || defaultValues.find(d => d.title === v.title)?.icon || 'Heart', title: v.title, description: v.description })) @@ -107,60 +105,122 @@ const AboutPage: React.FC = () => { const features = pageContent?.features && pageContent.features.length > 0 ? pageContent.features.map((f: any) => ({ - icon: defaultFeatures.find(d => d.title === f.title)?.icon || Star, + icon: f.icon || defaultFeatures.find(d => d.title === f.title)?.icon || 'Star', title: f.title, description: f.description })) : defaultFeatures; + // Parse JSON fields + const team = pageContent?.team && typeof pageContent.team === 'string' + ? JSON.parse(pageContent.team) + : (Array.isArray(pageContent?.team) ? pageContent.team : []); + const timeline = pageContent?.timeline && typeof pageContent.timeline === 'string' + ? JSON.parse(pageContent.timeline) + : (Array.isArray(pageContent?.timeline) ? pageContent.timeline : []); + const achievements = pageContent?.achievements && typeof pageContent.achievements === 'string' + ? JSON.parse(pageContent.achievements) + : (Array.isArray(pageContent?.achievements) ? pageContent.achievements : []); + + // Helper to get icon component + const getIconComponent = (iconName?: string) => { + if (!iconName) return Heart; + const IconComponent = (LucideIcons as any)[iconName] || Heart; + return IconComponent; + }; + return ( -
- {pageContent?.title || 'About Luxury Hotel'}
+
+
+ {pageContent?.title || 'About Luxury Hotel'}
+
-
+
{pageContent?.subtitle || pageContent?.description || 'Where Excellence Meets Unforgettable Experiences'}
+
+
+
+
+
+
+
+ Our Heritage
+
+
Our Story
-
+
+
+
+
+
-
+
{pageContent?.story_content ? (
- ') }} />
+ ') }}
+ />
) : (
<>
-
+
Welcome to Luxury Hotel, where timeless elegance meets modern sophistication.
Since our founding, we have been dedicated to providing exceptional hospitality
and creating unforgettable memories for our guests.
-
+
Nestled in the heart of the city, our hotel combines classic architecture with
contemporary amenities, offering a perfect blend of comfort and luxury. Every
detail has been carefully curated to ensure your stay exceeds expectations.
-
+
Our commitment to excellence extends beyond our beautiful rooms and facilities.
We believe in creating meaningful connections with our guests, understanding
their needs, and delivering personalized service that makes each visit special.
@@ -170,34 +230,49 @@ const AboutPage: React.FC = () => {
+
{/* Values Section */}
-
-
-
-
-
+
+
+
+
+
+
+ Core Principles
+
+
Our Values
-
+
+
+
+
+
-
+
{values.map((value, index) => (
-
-
+
+
+
+ {(() => {
+ const ValueIcon = getIconComponent(value.icon);
+ return ;
+ })()}
+
+
+ {value.title}
+
+
+ {value.description}
+
-
- {value.title}
-
-
- {value.description}
-
))}
@@ -206,60 +281,303 @@ const AboutPage: React.FC = () => {
{/* Features Section */}
-
-
-
-
-
+
+
+
+
+
+
+ Excellence Defined
+
+
Why Choose Us
-
+
+
+
+
+
-
- {features.map((feature, index) => (
-
-
-
+
+ {features.map((feature, index) => {
+ const FeatureIcon = getIconComponent(feature.icon);
+ return (
+
+
+
+
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
-
- {feature.title}
-
-
- {feature.description}
-
-
- ))}
+ );
+ })}
+ {/* Mission & Vision Section */}
+ {(pageContent?.mission || pageContent?.vision) && (
+
+
+
+
+
+
+
+ {pageContent.mission && (
+
+
+
+
+
+ Our Mission
+
+ {pageContent.mission}
+
+
+ )}
+ {pageContent.vision && (
+
+
+
+
+
+ Our Vision
+
+ {pageContent.vision}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Team Section */}
+ {team && team.length > 0 && (
+
+
+
+
+
+ Meet The Experts
+
+
+ Our Team
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Timeline Section */}
+ {timeline && timeline.length > 0 && (
+
+
+
+
+
+
+ Our Journey
+
+
+ Our History
+
+
+
+
+
+
+
+
+
+
+ {timeline.map((event: any, index: number) => (
+
+
+
+
+
+ {event.year}
+
+
+ {event.title}
+ {event.description}
+ {event.image && (
+
+
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Achievements Section */}
+ {achievements && achievements.length > 0 && (
+
+
+
+
+
+
+ Recognition
+
+
+ Achievements & Awards
+
+
+
+
+
+
+
+
+ {achievements.map((achievement: any, index: number) => {
+ const AchievementIcon = getIconComponent(achievement.icon);
+ return (
+
+
+
+
+
+
+
+ {achievement.year && (
+ {achievement.year}
+ )}
+
+ {achievement.title}
+ {achievement.description}
+ {achievement.image && (
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ )}
+
{/* Contact Section */}
-
-
-
-
-
+
+
+
+
+
+
+ Connect With Us
+
+
Get In Touch
-
-
+
+
+
+
+
+
We'd love to hear from you. Contact us for reservations or inquiries.
-
-
-
-
+
+
+
+
-
+
Address
-
+
{displayAddress
.split('\n').map((line, i) => (
@@ -269,40 +587,41 @@ const AboutPage: React.FC = () => {
))}
-
-
-
+
-
-
-
+
-
+
- Explore Our Rooms
-
+ Explore Our Rooms
+
+
diff --git a/Frontend/src/pages/ContactPage.tsx b/Frontend/src/pages/ContactPage.tsx
index 32668000..619729da 100644
--- a/Frontend/src/pages/ContactPage.tsx
+++ b/Frontend/src/pages/ContactPage.tsx
@@ -99,7 +99,7 @@ const ContactPage: React.FC = () => {
useEffect(() => {
const fetchPageContent = async () => {
try {
- const response = await pageContentService.getPageContent('contact');
+ const response = await pageContentService.getContactContent();
if (response.status === 'success' && response.data?.page_content) {
setPageContent(response.data.page_content);
diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx
index 723abf87..15ff2648 100644
--- a/Frontend/src/pages/HomePage.tsx
+++ b/Frontend/src/pages/HomePage.tsx
@@ -3,7 +3,13 @@ import { Link } from 'react-router-dom';
import {
ArrowRight,
AlertCircle,
+ Star,
+ X,
+ ChevronLeft,
+ ChevronRight,
+ ZoomIn,
} from 'lucide-react';
+import * as LucideIcons from 'lucide-react';
import {
BannerCarousel,
BannerSkeleton,
@@ -32,6 +38,33 @@ const HomePage: React.FC = () => {
const [isLoadingNewest, setIsLoadingNewest] = useState(true);
const [isLoadingContent, setIsLoadingContent] = useState(true);
const [error, setError] = useState(null);
+ const [lightboxOpen, setLightboxOpen] = useState(false);
+ const [lightboxIndex, setLightboxIndex] = useState(0);
+ const [lightboxImages, setLightboxImages] = useState([]);
+
+ // Handle keyboard navigation for lightbox
+ useEffect(() => {
+ if (!lightboxOpen) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setLightboxOpen(false);
+ } else if (e.key === 'ArrowLeft' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === 0 ? lightboxImages.length - 1 : prev - 1));
+ } else if (e.key === 'ArrowRight' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === lightboxImages.length - 1 ? 0 : prev + 1));
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ // Prevent body scroll when lightbox is open
+ document.body.style.overflow = 'hidden';
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = 'unset';
+ };
+ }, [lightboxOpen, lightboxImages.length]);
// Combine featured and newest rooms, removing duplicates
const combinedRooms = useMemo(() => {
@@ -57,22 +90,118 @@ const HomePage: React.FC = () => {
const fetchPageContent = async () => {
try {
setIsLoadingContent(true);
- const response = await pageContentService.getPageContent('home');
+ const response = await pageContentService.getHomeContent();
if (response.status === 'success' && response.data?.page_content) {
- setPageContent(response.data.page_content);
+ const content = response.data.page_content;
+
+ // Parse JSON fields if they come as strings (backward compatibility)
+ if (typeof content.features === 'string') {
+ try {
+ content.features = JSON.parse(content.features);
+ } catch (e) {
+ content.features = [];
+ }
+ }
+ if (typeof content.amenities === 'string') {
+ try {
+ content.amenities = JSON.parse(content.amenities);
+ } catch (e) {
+ content.amenities = [];
+ }
+ }
+ if (typeof content.testimonials === 'string') {
+ try {
+ content.testimonials = JSON.parse(content.testimonials);
+ } catch (e) {
+ content.testimonials = [];
+ }
+ }
+ if (typeof content.gallery_images === 'string') {
+ try {
+ content.gallery_images = JSON.parse(content.gallery_images);
+ } catch (e) {
+ content.gallery_images = [];
+ }
+ }
+ if (typeof content.stats === 'string') {
+ try {
+ content.stats = JSON.parse(content.stats);
+ } catch (e) {
+ content.stats = [];
+ }
+ }
+ // Parse luxury fields
+ if (typeof content.luxury_features === 'string') {
+ try {
+ content.luxury_features = JSON.parse(content.luxury_features);
+ } catch (e) {
+ content.luxury_features = [];
+ }
+ }
+ if (typeof content.luxury_gallery === 'string') {
+ try {
+ const parsed = JSON.parse(content.luxury_gallery);
+ content.luxury_gallery = Array.isArray(parsed) ? parsed.filter(img => img && typeof img === 'string' && img.trim() !== '') : [];
+ } catch (e) {
+ content.luxury_gallery = [];
+ }
+ }
+ // Ensure luxury_gallery is an array and filter out empty values
+ if (Array.isArray(content.luxury_gallery)) {
+ content.luxury_gallery = content.luxury_gallery.filter(img => img && typeof img === 'string' && img.trim() !== '');
+ } else {
+ content.luxury_gallery = [];
+ }
+ if (typeof content.luxury_testimonials === 'string') {
+ try {
+ content.luxury_testimonials = JSON.parse(content.luxury_testimonials);
+ } catch (e) {
+ content.luxury_testimonials = [];
+ }
+ }
+ if (typeof content.luxury_services === 'string') {
+ try {
+ content.luxury_services = JSON.parse(content.luxury_services);
+ } catch (e) {
+ content.luxury_services = [];
+ }
+ }
+ if (typeof content.luxury_experiences === 'string') {
+ try {
+ content.luxury_experiences = JSON.parse(content.luxury_experiences);
+ } catch (e) {
+ content.luxury_experiences = [];
+ }
+ }
+ if (typeof content.awards === 'string') {
+ try {
+ content.awards = JSON.parse(content.awards);
+ } catch (e) {
+ content.awards = [];
+ }
+ }
+ if (typeof content.partners === 'string') {
+ try {
+ content.partners = JSON.parse(content.partners);
+ } catch (e) {
+ content.partners = [];
+ }
+ }
+
+ setPageContent(content);
// Update document title and meta tags
- if (response.data.page_content.meta_title) {
- document.title = response.data.page_content.meta_title;
+ if (content.meta_title) {
+ document.title = content.meta_title;
}
- if (response.data.page_content.meta_description) {
+ if (content.meta_description) {
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
- metaDescription.setAttribute('content', response.data.page_content.meta_description);
+ metaDescription.setAttribute('content', content.meta_description);
}
}
} catch (err: any) {
@@ -215,27 +344,34 @@ const HomePage: React.FC = () => {
)}
-
+
+ {/* Subtle background pattern */}
+
+
{/* Featured & Newest Rooms Section - Combined Carousel */}
-
+
{/* Section Header - Centered */}
-
+
+
+
+
{pageContent?.hero_title || 'Featured & Newest Rooms'}
-
+
{pageContent?.hero_subtitle || pageContent?.description || 'Discover our most popular accommodations and latest additions'}
{/* View All Rooms Button - Golden, Centered */}
-
+
+
View All Rooms
-
+
@@ -300,79 +436,818 @@ const HomePage: React.FC = () => {
)}
- {/* Features Section */}
-
-
- {/* Decorative gold accent */}
-
-
-
-
-
- 🏨
-
-
- Easy Booking
-
-
- Search and book rooms with just a few clicks
-
-
+ {/* Features Section - Dynamic from page content */}
+ {(() => {
+ // Filter out empty features (no title or description)
+ const validFeatures = pageContent?.features?.filter(
+ (f: any) => f && (f.title || f.description)
+ ) || [];
+
+ return (validFeatures.length > 0 || !pageContent) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+ {/* Subtle background pattern */}
+
+
+
+ {validFeatures.length > 0 ? (
+ validFeatures.map((feature: any, index: number) => (
+
+ {feature.image ? (
+
+
+
+ ) : (
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+ {feature.title && (
+
+ {feature.title}
+
+ )}
+ {feature.description && (
+
+ {feature.description}
+
+ )}
+
+ ))
+ ) : (
+ <>
+
+
+ 🏨
+
+
+ Easy Booking
+
+
+ Search and book rooms with just a few clicks
+
+
-
-
- 💰
-
-
- Best Prices
-
-
- Best price guarantee in the market
-
-
+
+
+ 💰
+
+
+ Best Prices
+
+
+ Best price guarantee in the market
+
+
-
-
- 🎧
+
+
+ 🎧
+
+
+ 24/7 Support
+
+
+ Support team always ready to serve
+
+
+ >
+ )}
-
- 24/7 Support
-
-
- Support team always ready to serve
+
+
+ );
+ })()}
+
+ {/* Luxury Section - Dynamic from page content */}
+ {(pageContent?.luxury_section_title || (pageContent?.luxury_features && pageContent.luxury_features.length > 0)) && (
+
+
+
+
+
+
+ {pageContent.luxury_section_title || 'Experience Unparalleled Luxury'}
+
+ {pageContent.luxury_section_subtitle && (
+
+ {pageContent.luxury_section_subtitle}
+ )}
+
+ {pageContent.luxury_section_image && (
+
+
+
+
+
+
+ )}
+ {pageContent.luxury_features && pageContent.luxury_features.length > 0 && (
+
+ {pageContent.luxury_features.map((feature, index) => (
+
+
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Luxury Gallery Section - Dynamic from page content */}
+ {pageContent?.luxury_gallery && Array.isArray(pageContent.luxury_gallery) && pageContent.luxury_gallery.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_gallery_section_title || 'Luxury Gallery'}
+
+ {pageContent.luxury_gallery_section_subtitle && (
+
+ {pageContent.luxury_gallery_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_gallery.map((image, index) => {
+ // Normalize image URL - if it's a relative path, prepend the API URL
+ const imageUrl = image && typeof image === 'string'
+ ? (image.startsWith('http://') || image.startsWith('https://')
+ ? image
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${image.startsWith('/') ? image : '/' + image}`)
+ : '';
+
+ if (!imageUrl) return null;
+
+ return (
+ {
+ const normalizedImages = pageContent.luxury_gallery
+ .map(img => {
+ if (!img || typeof img !== 'string') return null;
+ return img.startsWith('http://') || img.startsWith('https://')
+ ? img
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${img.startsWith('/') ? img : '/' + img}`;
+ })
+ .filter(Boolean) as string[];
+ setLightboxImages(normalizedImages);
+ setLightboxIndex(index);
+ setLightboxOpen(true);
+ }}
+ >
+
{
+ console.error(`Failed to load luxury gallery image: ${imageUrl}`);
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Luxury Testimonials Section - Dynamic from page content */}
+ {pageContent?.luxury_testimonials && pageContent.luxury_testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_testimonials_section_title || 'Guest Experiences'}
+
+ {pageContent.luxury_testimonials_section_subtitle && (
+
+ {pageContent.luxury_testimonials_section_subtitle}
+
+ )}
+
+ Hear from our valued guests about their luxury stay
+
+
+
+ {pageContent.luxury_testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.title && (
+ {testimonial.title}
+ )}
+
+
+
+ "
+ {testimonial.quote}
+
+
+ ))}
+
+
+ )}
+
+ {/* Stats Section - Dynamic from page content */}
+ {pageContent?.stats && Array.isArray(pageContent.stats) && pageContent.stats.length > 0 && (
+
+
+ {/* Decorative elements */}
+
+
+
+
+ {pageContent.stats.map((stat, index) => (
+
+ {stat?.icon && (
+
+ {stat.icon && (LucideIcons as any)[stat.icon] ? (
+ React.createElement((LucideIcons as any)[stat.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ {stat.icon}
+ )}
+
+ )}
+ {stat?.number && (
+
+ {stat.number}
+
+ )}
+ {stat?.label && (
+
+ {stat.label}
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {/* Amenities Section - Dynamic from page content */}
+ {pageContent?.amenities && pageContent.amenities.length > 0 && (
+
+
+
+
+
+
+ {pageContent.amenities_section_title || 'Luxury Amenities'}
+
+
+ {pageContent.amenities_section_subtitle || 'Experience world-class amenities designed for your comfort'}
+
+
+
+ {pageContent.amenities.map((amenity, index) => (
+
+
+ {amenity.image ? (
+
+
+
+ ) : (
+
+ {amenity.icon && (LucideIcons as any)[amenity.icon] ? (
+ React.createElement((LucideIcons as any)[amenity.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {amenity.title}
+
+
+ {amenity.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Testimonials Section - Dynamic from page content */}
+ {pageContent?.testimonials && pageContent.testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.testimonials_section_title || 'Guest Testimonials'}
+
+
+ {pageContent.testimonials_section_subtitle || 'See what our guests say about their experience'}
+
+
+
+ {pageContent.testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.role}
+
+
+
+ {[...Array(5)].map((_, i) => (
+ ★
+ ))}
+
+
+ "
+ "{testimonial.comment}"
+
+
+ ))}
+
+
+ )}
+
+
+ {/* About Preview Section - Dynamic from page content */}
+ {(pageContent?.about_preview_title || pageContent?.about_preview_content) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+
+ {pageContent.about_preview_image && (
+
+
+
+
+ )}
+
+
+
+
+
+ {pageContent.about_preview_title || 'About Our Hotel'}
+
+ {pageContent.about_preview_subtitle && (
+
+ {pageContent.about_preview_subtitle}
+
+ )}
+ {pageContent.about_preview_content && (
+
+ {pageContent.about_preview_content}
+
+ )}
+
+
+ Learn More
+
+
+
+
+
+
+ )}
+
+ {/* Luxury Services Section */}
+ {pageContent?.luxury_services && pageContent.luxury_services.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_services_section_title || 'Luxury Services'}
+
+ {pageContent.luxury_services_section_subtitle && (
+
+ {pageContent.luxury_services_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_services.map((service: any, index: number) => (
+
+
+ {service.image ? (
+
+
+
+ ) : (
+
+ {service.icon && (LucideIcons as any)[service.icon] ? (
+ React.createElement((LucideIcons as any)[service.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {service.title}
+
+
+ {service.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Luxury Experiences Section */}
+ {pageContent?.luxury_experiences && pageContent.luxury_experiences.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_experiences_section_title || 'Unique Experiences'}
+
+ {pageContent.luxury_experiences_section_subtitle && (
+
+ {pageContent.luxury_experiences_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_experiences.map((experience: any, index: number) => (
+
+
+ {experience.image ? (
+
+
+
+ ) : (
+
+ {experience.icon && (LucideIcons as any)[experience.icon] ? (
+ React.createElement((LucideIcons as any)[experience.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {experience.title}
+
+
+ {experience.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Awards Section */}
+ {pageContent?.awards && pageContent.awards.length > 0 && (
+
+
+
+
+
+
+ {pageContent.awards_section_title || 'Awards & Recognition'}
+
+ {pageContent.awards_section_subtitle && (
+
+ {pageContent.awards_section_subtitle}
+
+ )}
+
+
+ {pageContent.awards.map((award: any, index: number) => (
+
+
+ {award.image ? (
+
+
+
+ ) : (
+
+ {award.icon && (LucideIcons as any)[award.icon] ? (
+ React.createElement((LucideIcons as any)[award.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ 🏆
+ )}
+
+ )}
+ {award.year && (
+ {award.year}
+ )}
+
+ {award.title}
+
+ {award.description && (
+
+ {award.description}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* CTA Section */}
+ {(pageContent?.cta_title || pageContent?.cta_subtitle) && (
+
+
+ {pageContent.cta_image && (
+
+
+
+ )}
+
+
+
+
+
+
+ {pageContent.cta_title}
+
+ {pageContent.cta_subtitle && (
+
+ {pageContent.cta_subtitle}
+
+ )}
+ {pageContent.cta_button_text && pageContent.cta_button_link && (
+
+
+ {pageContent.cta_button_text}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Partners Section */}
+ {pageContent?.partners && pageContent.partners.length > 0 && (
+
+
+
+
+
+
+ {pageContent.partners_section_title || 'Our Partners'}
+
+ {pageContent.partners_section_subtitle && (
+
+ {pageContent.partners_section_subtitle}
+
+ )}
+
+
+ {pageContent.partners.map((partner: any, index: number) => (
+
+ {partner.link ? (
+
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+
+ ) : (
+ <>
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+ >
+ )}
+
+ ))}
+
+
+ )}
+
+
+
+
+ {/* Luxury Gallery Lightbox Modal */}
+ {lightboxOpen && lightboxImages.length > 0 && (
+ setLightboxOpen(false)}
+ >
+ {/* Close Button */}
+
+
+ {/* Previous Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Next Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Image Container */}
+ e.stopPropagation()}
+ >
+
+
{
+ console.error(`Failed to load lightbox image: ${lightboxImages[lightboxIndex]}`);
+ }}
+ />
+
+ {/* Image Counter */}
+ {lightboxImages.length > 1 && (
+
+ {lightboxIndex + 1} / {lightboxImages.length}
+
+ )}
+
+ {/* Thumbnail Strip (if more than 1 image) */}
+ {lightboxImages.length > 1 && lightboxImages.length <= 10 && (
+
+ {lightboxImages.map((thumb, idx) => (
+
+ ))}
+
+ )}
-
-
+ )}
>
);
};
diff --git a/Frontend/src/pages/admin/BookingManagementPage.tsx b/Frontend/src/pages/admin/BookingManagementPage.tsx
index 9c7bd13f..ce037b6f 100644
--- a/Frontend/src/pages/admin/BookingManagementPage.tsx
+++ b/Frontend/src/pages/admin/BookingManagementPage.tsx
@@ -1,20 +1,23 @@
import React, { useEffect, useState } from 'react';
-import { Search, Eye, XCircle, CheckCircle, Loader2 } from 'lucide-react';
-import { bookingService, Booking } from '../../services/api';
+import { Search, Eye, XCircle, CheckCircle, Loader2, FileText } from 'lucide-react';
+import { bookingService, Booking, invoiceService } from '../../services/api';
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import Pagination from '../../components/common/Pagination';
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { parseDateLocal } from '../../utils/format';
+import { useNavigate } from 'react-router-dom';
const BookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
+ const navigate = useNavigate();
const [bookings, setBookings] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedBooking, setSelectedBooking] = useState(null);
const [showDetailModal, setShowDetailModal] = useState(false);
const [updatingBookingId, setUpdatingBookingId] = useState(null);
const [cancellingBookingId, setCancellingBookingId] = useState(null);
+ const [creatingInvoice, setCreatingInvoice] = useState(false);
const [filters, setFilters] = useState({
search: '',
status: '',
@@ -80,6 +83,32 @@ const BookingManagementPage: React.FC = () => {
}
};
+ const handleCreateInvoice = async (bookingId: number) => {
+ try {
+ setCreatingInvoice(true);
+ // Ensure bookingId is a number
+ const invoiceData = {
+ booking_id: Number(bookingId),
+ };
+
+ const response = await invoiceService.createInvoice(invoiceData);
+
+ if (response.status === 'success' && response.data?.invoice) {
+ toast.success('Invoice created successfully!');
+ setShowDetailModal(false);
+ navigate(`/admin/invoices/${response.data.invoice.id}`);
+ } else {
+ throw new Error('Failed to create invoice');
+ }
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
+ toast.error(errorMessage);
+ console.error('Invoice creation error:', error);
+ } finally {
+ setCreatingInvoice(false);
+ }
+ };
+
const getStatusBadge = (status: string) => {
const badges: Record = {
pending: {
@@ -622,7 +651,24 @@ const BookingManagementPage: React.FC = () => {
{/* Modal Footer */}
-
+
+
- {/* Search Booking */}
+ {/* Date and Search Filters */}
- 1. Search booking
-
-
-
+
+
+
setBookingNumber(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
- placeholder="Enter booking number"
- className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ type="date"
+ value={selectedDate}
+ onChange={(e) => setSelectedDate(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
-
+
+
+
+ setSearchQuery(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSearchByNumber()}
+ placeholder="Enter booking number"
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+
+
- {/* Booking Info */}
+ {/* Check-ins and Check-outs Lists */}
+ {!booking && (
+
+ {/* Check-ins for Today */}
+
+
+
+
+ Check-ins for {formatDate(selectedDate)}
+
+
+ {checkInBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkInBookings.length === 0 ? (
+
+
+ No check-ins scheduled for this date
+
+ ) : (
+
+ {checkInBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Check-outs for Today */}
+
+
+
+
+ Check-outs for {formatDate(selectedDate)}
+
+
+ {checkOutBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkOutBookings.length === 0 ? (
+
+
+ No check-outs scheduled for this date
+
+ ) : (
+
+ {checkOutBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-orange-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Booking Info and Check-in Form */}
{booking && (
<>
-
-
- 2. Booking Information
-
+
+
+
+ 2. Booking Information
+
+
+
@@ -299,7 +541,6 @@ const CheckInPage: React.FC = () => {
{(() => {
- // Use payment_balance from API if available, otherwise calculate from payments
const paymentBalance = booking.payment_balance || (() => {
const completedPayments = booking.payments?.filter(
(p) => p.payment_status === 'completed'
@@ -545,19 +786,6 @@ const CheckInPage: React.FC = () => {
>
)}
-
- {/* Empty State */}
- {!booking && !searching && (
-
-
-
- No booking selected
-
-
- Please enter booking number above to start check-in process
-
-
- )}
);
};
diff --git a/Frontend/src/pages/admin/InvoiceManagementPage.tsx b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
index 3d897a7a..e7d6245d 100644
--- a/Frontend/src/pages/admin/InvoiceManagementPage.tsx
+++ b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
@@ -47,7 +47,8 @@ const InvoiceManagementPage: React.FC = () => {
invoiceList = invoiceList.filter((inv) =>
inv.invoice_number.toLowerCase().includes(filters.search.toLowerCase()) ||
inv.customer_name.toLowerCase().includes(filters.search.toLowerCase()) ||
- inv.customer_email.toLowerCase().includes(filters.search.toLowerCase())
+ inv.customer_email.toLowerCase().includes(filters.search.toLowerCase()) ||
+ (inv.promotion_code && inv.promotion_code.toLowerCase().includes(filters.search.toLowerCase()))
);
}
@@ -130,11 +131,11 @@ const InvoiceManagementPage: React.FC = () => {
Manage and track all invoices
@@ -190,6 +191,9 @@ const InvoiceManagementPage: React.FC = () => {
Amount
+
+ Promotion
+
Status
@@ -235,6 +239,25 @@ const InvoiceManagementPage: React.FC = () => {
Due: {formatCurrency(invoice.balance_due)}
)}
+ {invoice.discount_amount > 0 && (
+
+ Discount: -{formatCurrency(invoice.discount_amount)}
+
+ )}
+
+
+ {invoice.promotion_code ? (
+
+ {invoice.promotion_code}
+
+ ) : (
+ —
+ )}
+ {invoice.is_proforma && (
+
+ Proforma
+
+ )}
@@ -274,7 +297,7 @@ const InvoiceManagementPage: React.FC = () => {
})
) : (
-
+
No invoices found
diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx
index d52089e8..d33edbec 100644
--- a/Frontend/src/pages/admin/PageContentDashboard.tsx
+++ b/Frontend/src/pages/admin/PageContentDashboard.tsx
@@ -7,13 +7,6 @@ import {
Search,
Save,
Globe,
- Facebook,
- Twitter,
- Instagram,
- Linkedin,
- Youtube,
- MapPin,
- Phone,
X,
Plus,
Trash2,
@@ -31,6 +24,8 @@ import { pageContentService, PageContent, PageType, UpdatePageContentData, banne
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import { ConfirmationDialog } from '../../components/common';
+import IconPicker from '../../components/admin/IconPicker';
+import { luxuryContentSeed } from '../../data/luxuryContentSeed';
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo';
@@ -62,7 +57,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -113,6 +108,21 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Helper function to normalize arrays (handle both arrays and JSON strings)
+ const normalizeArray = (value: any): any[] => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ if (typeof value === 'string') {
+ try {
+ const parsed = JSON.parse(value);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+ }
+ return [];
+ };
+
const initializeFormData = (contents: Record) => {
// Home
if (contents.home) {
@@ -130,6 +140,45 @@ const PageContentDashboard: React.FC = () => {
og_title: contents.home.og_title || '',
og_description: contents.home.og_description || '',
og_image: contents.home.og_image || '',
+ features: normalizeArray(contents.home.features),
+ amenities_section_title: contents.home.amenities_section_title || '',
+ amenities_section_subtitle: contents.home.amenities_section_subtitle || '',
+ amenities: normalizeArray(contents.home.amenities),
+ testimonials_section_title: contents.home.testimonials_section_title || '',
+ testimonials_section_subtitle: contents.home.testimonials_section_subtitle || '',
+ testimonials: normalizeArray(contents.home.testimonials),
+ about_preview_title: contents.home.about_preview_title || '',
+ about_preview_subtitle: contents.home.about_preview_subtitle || '',
+ about_preview_content: contents.home.about_preview_content || '',
+ about_preview_image: contents.home.about_preview_image || '',
+ stats: normalizeArray(contents.home.stats),
+ luxury_section_title: contents.home.luxury_section_title || '',
+ luxury_section_subtitle: contents.home.luxury_section_subtitle || '',
+ luxury_section_image: contents.home.luxury_section_image || '',
+ luxury_features: normalizeArray(contents.home.luxury_features),
+ luxury_gallery_section_title: contents.home.luxury_gallery_section_title || '',
+ luxury_gallery_section_subtitle: contents.home.luxury_gallery_section_subtitle || '',
+ luxury_gallery: normalizeArray(contents.home.luxury_gallery),
+ luxury_testimonials_section_title: contents.home.luxury_testimonials_section_title || '',
+ luxury_testimonials_section_subtitle: contents.home.luxury_testimonials_section_subtitle || '',
+ luxury_testimonials: normalizeArray(contents.home.luxury_testimonials),
+ luxury_services_section_title: contents.home.luxury_services_section_title || '',
+ luxury_services_section_subtitle: contents.home.luxury_services_section_subtitle || '',
+ luxury_services: normalizeArray(contents.home.luxury_services),
+ luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
+ luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
+ luxury_experiences: normalizeArray(contents.home.luxury_experiences),
+ awards_section_title: contents.home.awards_section_title || '',
+ awards_section_subtitle: contents.home.awards_section_subtitle || '',
+ awards: normalizeArray(contents.home.awards),
+ cta_title: contents.home.cta_title || '',
+ cta_subtitle: contents.home.cta_subtitle || '',
+ cta_button_text: contents.home.cta_button_text || '',
+ cta_button_link: contents.home.cta_button_link || '',
+ cta_image: contents.home.cta_image || '',
+ partners_section_title: contents.home.partners_section_title || '',
+ partners_section_subtitle: contents.home.partners_section_subtitle || '',
+ partners: normalizeArray(contents.home.partners),
});
}
@@ -154,8 +203,14 @@ const PageContentDashboard: React.FC = () => {
description: contents.about.description || '',
content: contents.about.content || '',
story_content: contents.about.story_content || '',
- values: contents.about.values || [],
- features: contents.about.features || [],
+ values: normalizeArray(contents.about.values),
+ features: normalizeArray(contents.about.features),
+ about_hero_image: contents.about.about_hero_image || '',
+ mission: contents.about.mission || '',
+ vision: contents.about.vision || '',
+ team: normalizeArray(contents.about.team),
+ timeline: normalizeArray(contents.about.timeline),
+ achievements: normalizeArray(contents.about.achievements),
meta_title: contents.about.meta_title || '',
meta_description: contents.about.meta_description || '',
});
@@ -169,6 +224,7 @@ const PageContentDashboard: React.FC = () => {
social_links: contents.footer.social_links || {},
footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] },
badges: contents.footer.badges || [],
+ copyright_text: contents.footer.copyright_text || '',
meta_title: contents.footer.meta_title || '',
meta_description: contents.footer.meta_description || '',
});
@@ -244,7 +300,7 @@ const PageContentDashboard: React.FC = () => {
try {
setUploadingImage(true);
const response = await bannerService.uploadBannerImage(file);
- if (response.status === 'success' || response.success) {
+ if (response.success) {
setBannerFormData({ ...bannerFormData, image_url: response.data.image_url });
toast.success('Image uploaded successfully');
}
@@ -257,6 +313,27 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Generic image upload handler for page content images
+ const handlePageContentImageUpload = async (
+ file: File,
+ onSuccess: (imageUrl: string) => void
+ ) => {
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error('Image size must be less than 5MB');
+ return;
+ }
+
+ try {
+ const response = await pageContentService.uploadImage(file);
+ if (response.success) {
+ onSuccess(response.data.image_url);
+ toast.success('Image uploaded successfully');
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to upload image');
+ }
+ };
+
const handleBannerSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -270,7 +347,7 @@ const PageContentDashboard: React.FC = () => {
if (imageFile && !imageUrl) {
setUploadingImage(true);
const uploadResponse = await bannerService.uploadBannerImage(imageFile);
- if (uploadResponse.status === 'success' || uploadResponse.success) {
+ if (uploadResponse.success) {
imageUrl = uploadResponse.data.image_url;
} else {
throw new Error('Failed to upload image');
@@ -305,9 +382,9 @@ const PageContentDashboard: React.FC = () => {
setEditingBanner(banner);
setBannerFormData({
title: banner.title || '',
- description: '',
+ description: banner.description || '',
image_url: banner.image_url || '',
- link: banner.link || '',
+ link_url: banner.link_url || '',
position: banner.position || 'home',
display_order: banner.display_order || 0,
is_active: banner.is_active ?? true,
@@ -343,7 +420,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -531,6 +608,42 @@ const PageContentDashboard: React.FC = () => {
{/* Home Tab */}
{activeTab === 'home' && (
+ {/* Seed Data Button */}
+
+
+
+ Quick Start
+ Load pre-configured luxury content to get started quickly
+
+
+
+
+
{/* Home Page Content Section */}
Home Page Content
@@ -559,14 +672,38 @@ const PageContentDashboard: React.FC = () => {
-
- setHomeData({ ...homeData, hero_image: e.target.value })}
- className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
- placeholder="https://example.com/hero-image.jpg"
- />
+
+
+ setHomeData({ ...homeData, hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {homeData.hero_image && (
+
+
+
+ )}
@@ -621,17 +758,1388 @@ const PageContentDashboard: React.FC = () => {
placeholder="SEO Meta Description"
/>
+
+
-
-
+ {/* Amenities Section */}
+
+ Amenities Section
+
+
+
+ setHomeData({ ...homeData, amenities_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Amenities"
+ />
+
+
+ setHomeData({ ...homeData, amenities_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience world-class amenities"
+ />
+
+
+
+ Amenities
+
+
+
+ {Array.isArray(homeData.amenities) && homeData.amenities.map((amenity, index) => (
+
+
+ Amenity {index + 1}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], icon: iconName };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], image: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {amenity?.image && (
+
+
+
+ )}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], title: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.amenities || homeData.amenities.length === 0) && (
+ No amenities added yet. Click "Add Amenity" to get started.
+ )}
+
+
+
+ {/* Luxury Section */}
+
+ Luxury Section
+
+
+
+ setHomeData({ ...homeData, luxury_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience Unparalleled Luxury"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Where elegance meets comfort in every detail"
+ />
+
+
+
+
+ setHomeData({ ...homeData, luxury_section_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/luxury-image.jpg or upload"
+ />
+
+
+ {homeData.luxury_section_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Luxury Features Section */}
+
+
+ Luxury Features
+
+
+
+ {Array.isArray(homeData.luxury_features) && homeData.luxury_features.map((feature, index) => (
+
+
+ Luxury Feature {index + 1}
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], icon: iconName };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], title: e.target.value };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Feature Title"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_features || homeData.luxury_features.length === 0) && (
+ No luxury features added yet. Click "Add Luxury Feature" to get started.
+ )}
+
+
+
+ {/* Luxury Gallery Section */}
+
+
+ Luxury Gallery
+
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Gallery"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Discover our exquisite spaces and amenities"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_gallery) && homeData.luxury_gallery.map((image, index) => (
+
+ {
+ setHomeData((prevData) => {
+ const currentGallery = Array.isArray(prevData.luxury_gallery) ? [...prevData.luxury_gallery] : [];
+ currentGallery[index] = e.target.value;
+ return { ...prevData, luxury_gallery: currentGallery };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {image && (
+
+
+
+ )}
+
+ ))}
+ {(!homeData.luxury_gallery || homeData.luxury_gallery.length === 0) && (
+ No gallery images added yet. Click "Add Gallery Image" to get started.
+ )}
+
+
+
+ {/* Luxury Testimonials Section */}
+
+
+ Luxury Testimonials
+
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Guest Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Hear from our valued guests about their luxury stay"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_testimonials) && homeData.luxury_testimonials.map((testimonial, index) => (
+
+
+ Luxury Testimonial {index + 1}
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], name: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], title: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], image: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {testimonial?.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_testimonials || homeData.luxury_testimonials.length === 0) && (
+ No luxury testimonials added yet. Click "Add Luxury Testimonial" to get started.
+ )}
+
+
+
+ {/* About Preview Section */}
+
+ About Preview Section
+
+
+
+ setHomeData({ ...homeData, about_preview_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="About Our Hotel"
+ />
+
+
+
+ setHomeData({ ...homeData, about_preview_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ />
+
+
+
+
+
+
+
+ setHomeData({ ...homeData, about_preview_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/about-image.jpg or upload"
+ />
+
+
+ {homeData.about_preview_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Stats Section */}
+
+
+ Statistics Section
+
+
+
+ {Array.isArray(homeData.stats) && homeData.stats.map((stat, index) => (
+
+
+ Stat {index + 1}
+
+
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], number: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="1000+"
+ />
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], label: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Happy Guests"
+ />
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], icon: iconName };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ label="Icon"
+ />
+
+
+
+ ))}
+ {(!homeData.stats || homeData.stats.length === 0) && (
+ No statistics added yet. Click "Add Stat" to get started.
+ )}
+
+
+
+ {/* Luxury Services Section */}
+
+
+ Luxury Services
+
+
+
+ {Array.isArray(homeData.luxury_services) && homeData.luxury_services.map((service, index) => (
+
+
+ Service {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {service?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Service Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_services || homeData.luxury_services.length === 0) && (
+ No services added yet. Click "Add Service" to get started.
+ )}
+
+
+
+ {/* Luxury Experiences Section */}
+
+
+ Luxury Experiences
+
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unique Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unforgettable moments await"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_experiences) && homeData.luxury_experiences.map((experience, index) => (
+
+
+ Experience {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {experience?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Experience Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_experiences || homeData.luxury_experiences.length === 0) && (
+ No experiences added yet. Click "Add Experience" to get started.
+ )}
+
+
+
+ {/* Awards Section */}
+
+
+ Awards & Certifications
+
+
+
+
+ setHomeData({ ...homeData, awards_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Awards & Recognition"
+ />
+
+
+
+ setHomeData({ ...homeData, awards_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Recognized excellence in hospitality"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.awards) && homeData.awards.map((award, index) => (
+
+
+ Award {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], year: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="2024"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {award?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Award Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.awards || homeData.awards.length === 0) && (
+ No awards added yet. Click "Add Award" to get started.
+ )}
+
+
+
+ {/* CTA Section */}
+
+ Call to Action Section
+
+
+
+ setHomeData({ ...homeData, cta_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Ready to Experience Luxury?"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book your stay today"
+ />
+
+
+
+
+ setHomeData({ ...homeData, cta_button_text: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book Now"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_button_link: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="/rooms"
+ />
+
+
+
+
+
+ setHomeData({ ...homeData, cta_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/cta-image.jpg or upload"
+ />
+
+
+ {homeData.cta_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Partners Section */}
+
+
+ Partners & Brands
+
+
+
+
+ setHomeData({ ...homeData, partners_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Our Partners"
+ />
+
+
+
+ setHomeData({ ...homeData, partners_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Trusted by leading brands"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.partners) && homeData.partners.map((partner, index) => (
+
+
+ Partner {index + 1}
+
+
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], name: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Partner Name"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], logo: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com/logo.png"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], link: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com"
+ />
+
+
+
+ ))}
+ {(!homeData.partners || homeData.partners.length === 0) && (
+ No partners added yet. Click "Add Partner" to get started.
+ )}
+
+
+
+ {/* Save Button */}
+
+
+
@@ -696,8 +2204,11 @@ const PageContentDashboard: React.FC = () => {
{banner.title}
- {banner.link && (
- {banner.link}
+ {banner.description && (
+ {banner.description}
+ )}
+ {banner.link_url && (
+ {banner.link_url}
)}
@@ -781,12 +2292,24 @@ const PageContentDashboard: React.FC = () => {
/>
+
+
+
+
setBannerFormData({ ...bannerFormData, link: e.target.value })}
+ value={bannerFormData.link_url}
+ onChange={(e) => setBannerFormData({ ...bannerFormData, link_url: e.target.value })}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
placeholder="https://example.com"
/>
@@ -1140,6 +2663,690 @@ const PageContentDashboard: React.FC = () => {
/>
+ {/* Hero Image */}
+
+
+
+ setAboutData({ ...aboutData, about_hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {aboutData.about_hero_image && (
+
+
+
+ )}
+
+
+ {/* Mission */}
+
+
+
+
+ {/* Vision */}
+
+
+
+
+ {/* Values Section */}
+
+
+ Our Values
+
+
+ {Array.isArray(aboutData.values) && aboutData.values.length > 0 ? (
+
+ {aboutData.values.map((value, index) => (
+
+
+ Value {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], icon };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], title: e.target.value };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Value title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No values added yet. Click "Add Value" to get started.
+ )}
+
+
+ {/* Features Section */}
+
+
+ Features
+
+
+ {Array.isArray(aboutData.features) && aboutData.features.length > 0 ? (
+
+ {aboutData.features.map((feature, index) => (
+
+
+ Feature {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], icon };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], title: e.target.value };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Feature title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No features added yet. Click "Add Feature" to get started.
+ )}
+
+
+ {/* Team Section */}
+
+
+ Team Members
+
+
+ {Array.isArray(aboutData.team) && aboutData.team.length > 0 ? (
+
+ {aboutData.team.map((member, index) => (
+
+
+ Team Member {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], name: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Full name"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], role: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Job title"
+ />
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], image: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {member.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+
+ ) : (
+ No team members added yet. Click "Add Team Member" to get started.
+ )}
+
+
+ {/* Timeline Section */}
+
+
+ Timeline / History
+
+
+ {Array.isArray(aboutData.timeline) && aboutData.timeline.length > 0 ? (
+
+ {aboutData.timeline.map((event, index) => (
+
+
+ Event {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], year: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], title: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Event title"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], image: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {event.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No timeline events added yet. Click "Add Event" to get started.
+ )}
+
+
+ {/* Achievements Section */}
+
+
+ Achievements & Awards
+
+
+ {Array.isArray(aboutData.achievements) && aboutData.achievements.length > 0 ? (
+
+ {aboutData.achievements.map((achievement, index) => (
+
+
+ Achievement {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], icon };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ />
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], title: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Achievement title"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], year: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], image: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {achievement.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No achievements added yet. Click "Add Achievement" to get started.
+ )}
+
+
@@ -1400,6 +3607,28 @@ const PageContentDashboard: React.FC = () => {
+ {/* Copyright Text */}
+
+ Copyright Text
+
+
+
+ setFooterData({ ...footerData, copyright_text: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200"
+ placeholder="© {YEAR} Luxury Hotel. All rights reserved."
+ />
+
+ The {`{YEAR}`} placeholder will be automatically replaced with the current year.
+
+
+
+
+
Our Story
- ++
Welcome to Luxury Hotel, where timeless elegance meets modern sophistication. Since our founding, we have been dedicated to providing exceptional hospitality and creating unforgettable memories for our guests.
-+
Nestled in the heart of the city, our hotel combines classic architecture with contemporary amenities, offering a perfect blend of comfort and luxury. Every detail has been carefully curated to ensure your stay exceeds expectations.
-+
Our commitment to excellence extends beyond our beautiful rooms and facilities. We believe in creating meaningful connections with our guests, understanding their needs, and delivering personalized service that makes each visit special. @@ -170,34 +230,49 @@ const AboutPage: React.FC = () => {
+
+
+
+
+
+
+ Core Principles
+
+
Our Values
-
+
+
+
+
+
-
+
{values.map((value, index) => (
-
-
+
+
+
+ {(() => {
+ const ValueIcon = getIconComponent(value.icon);
+ return ;
+ })()}
+
+
+ {value.title}
+
+
+ {value.description}
+
-
- {value.title}
-
-
- {value.description}
-
))}
@@ -206,60 +281,303 @@ const AboutPage: React.FC = () => {
{/* Features Section */}
-
-
-
-
-
+
+
+
+
+
+
+ Excellence Defined
+
+
Why Choose Us
-
+
+
+
+
+
-
- {features.map((feature, index) => (
-
-
-
+
+ {features.map((feature, index) => {
+ const FeatureIcon = getIconComponent(feature.icon);
+ return (
+
+
+
+
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
-
- {feature.title}
-
-
- {feature.description}
-
-
- ))}
+ );
+ })}
+ {/* Mission & Vision Section */}
+ {(pageContent?.mission || pageContent?.vision) && (
+
+
+
+
+
+
+
+ {pageContent.mission && (
+
+
+
+
+
+ Our Mission
+
+ {pageContent.mission}
+
+
+ )}
+ {pageContent.vision && (
+
+
+
+
+
+ Our Vision
+
+ {pageContent.vision}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Team Section */}
+ {team && team.length > 0 && (
+
+
+
+
+
+ Meet The Experts
+
+
+ Our Team
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Timeline Section */}
+ {timeline && timeline.length > 0 && (
+
+
+
+
+
+
+ Our Journey
+
+
+ Our History
+
+
+
+
+
+
+
+
+
+
+ {timeline.map((event: any, index: number) => (
+
+
+
+
+
+ {event.year}
+
+
+ {event.title}
+ {event.description}
+ {event.image && (
+
+
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Achievements Section */}
+ {achievements && achievements.length > 0 && (
+
+
+
+
+
+
+ Recognition
+
+
+ Achievements & Awards
+
+
+
+
+
+
+
+
+ {achievements.map((achievement: any, index: number) => {
+ const AchievementIcon = getIconComponent(achievement.icon);
+ return (
+
+
+
+
+
+
+
+ {achievement.year && (
+ {achievement.year}
+ )}
+
+ {achievement.title}
+ {achievement.description}
+ {achievement.image && (
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ )}
+
{/* Contact Section */}
-
-
-
-
-
+
+
+
+
+
+
+ Connect With Us
+
+
Get In Touch
-
-
+
+
+
+
+
+
We'd love to hear from you. Contact us for reservations or inquiries.
-
-
-
-
+
+
+
+
-
+
Address
-
+
{displayAddress
.split('\n').map((line, i) => (
@@ -269,40 +587,41 @@ const AboutPage: React.FC = () => {
))}
-
-
-
+
-
-
-
+
-
+
- Explore Our Rooms
-
+ Explore Our Rooms
+
+
diff --git a/Frontend/src/pages/ContactPage.tsx b/Frontend/src/pages/ContactPage.tsx
index 32668000..619729da 100644
--- a/Frontend/src/pages/ContactPage.tsx
+++ b/Frontend/src/pages/ContactPage.tsx
@@ -99,7 +99,7 @@ const ContactPage: React.FC = () => {
useEffect(() => {
const fetchPageContent = async () => {
try {
- const response = await pageContentService.getPageContent('contact');
+ const response = await pageContentService.getContactContent();
if (response.status === 'success' && response.data?.page_content) {
setPageContent(response.data.page_content);
diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx
index 723abf87..15ff2648 100644
--- a/Frontend/src/pages/HomePage.tsx
+++ b/Frontend/src/pages/HomePage.tsx
@@ -3,7 +3,13 @@ import { Link } from 'react-router-dom';
import {
ArrowRight,
AlertCircle,
+ Star,
+ X,
+ ChevronLeft,
+ ChevronRight,
+ ZoomIn,
} from 'lucide-react';
+import * as LucideIcons from 'lucide-react';
import {
BannerCarousel,
BannerSkeleton,
@@ -32,6 +38,33 @@ const HomePage: React.FC = () => {
const [isLoadingNewest, setIsLoadingNewest] = useState(true);
const [isLoadingContent, setIsLoadingContent] = useState(true);
const [error, setError] = useState(null);
+ const [lightboxOpen, setLightboxOpen] = useState(false);
+ const [lightboxIndex, setLightboxIndex] = useState(0);
+ const [lightboxImages, setLightboxImages] = useState([]);
+
+ // Handle keyboard navigation for lightbox
+ useEffect(() => {
+ if (!lightboxOpen) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setLightboxOpen(false);
+ } else if (e.key === 'ArrowLeft' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === 0 ? lightboxImages.length - 1 : prev - 1));
+ } else if (e.key === 'ArrowRight' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === lightboxImages.length - 1 ? 0 : prev + 1));
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ // Prevent body scroll when lightbox is open
+ document.body.style.overflow = 'hidden';
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = 'unset';
+ };
+ }, [lightboxOpen, lightboxImages.length]);
// Combine featured and newest rooms, removing duplicates
const combinedRooms = useMemo(() => {
@@ -57,22 +90,118 @@ const HomePage: React.FC = () => {
const fetchPageContent = async () => {
try {
setIsLoadingContent(true);
- const response = await pageContentService.getPageContent('home');
+ const response = await pageContentService.getHomeContent();
if (response.status === 'success' && response.data?.page_content) {
- setPageContent(response.data.page_content);
+ const content = response.data.page_content;
+
+ // Parse JSON fields if they come as strings (backward compatibility)
+ if (typeof content.features === 'string') {
+ try {
+ content.features = JSON.parse(content.features);
+ } catch (e) {
+ content.features = [];
+ }
+ }
+ if (typeof content.amenities === 'string') {
+ try {
+ content.amenities = JSON.parse(content.amenities);
+ } catch (e) {
+ content.amenities = [];
+ }
+ }
+ if (typeof content.testimonials === 'string') {
+ try {
+ content.testimonials = JSON.parse(content.testimonials);
+ } catch (e) {
+ content.testimonials = [];
+ }
+ }
+ if (typeof content.gallery_images === 'string') {
+ try {
+ content.gallery_images = JSON.parse(content.gallery_images);
+ } catch (e) {
+ content.gallery_images = [];
+ }
+ }
+ if (typeof content.stats === 'string') {
+ try {
+ content.stats = JSON.parse(content.stats);
+ } catch (e) {
+ content.stats = [];
+ }
+ }
+ // Parse luxury fields
+ if (typeof content.luxury_features === 'string') {
+ try {
+ content.luxury_features = JSON.parse(content.luxury_features);
+ } catch (e) {
+ content.luxury_features = [];
+ }
+ }
+ if (typeof content.luxury_gallery === 'string') {
+ try {
+ const parsed = JSON.parse(content.luxury_gallery);
+ content.luxury_gallery = Array.isArray(parsed) ? parsed.filter(img => img && typeof img === 'string' && img.trim() !== '') : [];
+ } catch (e) {
+ content.luxury_gallery = [];
+ }
+ }
+ // Ensure luxury_gallery is an array and filter out empty values
+ if (Array.isArray(content.luxury_gallery)) {
+ content.luxury_gallery = content.luxury_gallery.filter(img => img && typeof img === 'string' && img.trim() !== '');
+ } else {
+ content.luxury_gallery = [];
+ }
+ if (typeof content.luxury_testimonials === 'string') {
+ try {
+ content.luxury_testimonials = JSON.parse(content.luxury_testimonials);
+ } catch (e) {
+ content.luxury_testimonials = [];
+ }
+ }
+ if (typeof content.luxury_services === 'string') {
+ try {
+ content.luxury_services = JSON.parse(content.luxury_services);
+ } catch (e) {
+ content.luxury_services = [];
+ }
+ }
+ if (typeof content.luxury_experiences === 'string') {
+ try {
+ content.luxury_experiences = JSON.parse(content.luxury_experiences);
+ } catch (e) {
+ content.luxury_experiences = [];
+ }
+ }
+ if (typeof content.awards === 'string') {
+ try {
+ content.awards = JSON.parse(content.awards);
+ } catch (e) {
+ content.awards = [];
+ }
+ }
+ if (typeof content.partners === 'string') {
+ try {
+ content.partners = JSON.parse(content.partners);
+ } catch (e) {
+ content.partners = [];
+ }
+ }
+
+ setPageContent(content);
// Update document title and meta tags
- if (response.data.page_content.meta_title) {
- document.title = response.data.page_content.meta_title;
+ if (content.meta_title) {
+ document.title = content.meta_title;
}
- if (response.data.page_content.meta_description) {
+ if (content.meta_description) {
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
- metaDescription.setAttribute('content', response.data.page_content.meta_description);
+ metaDescription.setAttribute('content', content.meta_description);
}
}
} catch (err: any) {
@@ -215,27 +344,34 @@ const HomePage: React.FC = () => {
)}
-
+
+ {/* Subtle background pattern */}
+
+
{/* Featured & Newest Rooms Section - Combined Carousel */}
-
+
{/* Section Header - Centered */}
-
+
+
+
+
{pageContent?.hero_title || 'Featured & Newest Rooms'}
-
+
{pageContent?.hero_subtitle || pageContent?.description || 'Discover our most popular accommodations and latest additions'}
{/* View All Rooms Button - Golden, Centered */}
-
+
+
View All Rooms
-
+
@@ -300,79 +436,818 @@ const HomePage: React.FC = () => {
)}
- {/* Features Section */}
-
-
- {/* Decorative gold accent */}
-
-
-
-
-
- 🏨
-
-
- Easy Booking
-
-
- Search and book rooms with just a few clicks
-
-
+ {/* Features Section - Dynamic from page content */}
+ {(() => {
+ // Filter out empty features (no title or description)
+ const validFeatures = pageContent?.features?.filter(
+ (f: any) => f && (f.title || f.description)
+ ) || [];
+
+ return (validFeatures.length > 0 || !pageContent) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+ {/* Subtle background pattern */}
+
+
+
+ {validFeatures.length > 0 ? (
+ validFeatures.map((feature: any, index: number) => (
+
+ {feature.image ? (
+
+
+
+ ) : (
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+ {feature.title && (
+
+ {feature.title}
+
+ )}
+ {feature.description && (
+
+ {feature.description}
+
+ )}
+
+ ))
+ ) : (
+ <>
+
+
+ 🏨
+
+
+ Easy Booking
+
+
+ Search and book rooms with just a few clicks
+
+
-
-
- 💰
-
-
- Best Prices
-
-
- Best price guarantee in the market
-
-
+
+
+ 💰
+
+
+ Best Prices
+
+
+ Best price guarantee in the market
+
+
-
-
- 🎧
+
+
+ 🎧
+
+
+ 24/7 Support
+
+
+ Support team always ready to serve
+
+
+ >
+ )}
-
- 24/7 Support
-
-
- Support team always ready to serve
+
+
+ );
+ })()}
+
+ {/* Luxury Section - Dynamic from page content */}
+ {(pageContent?.luxury_section_title || (pageContent?.luxury_features && pageContent.luxury_features.length > 0)) && (
+
+
+
+
+
+
+ {pageContent.luxury_section_title || 'Experience Unparalleled Luxury'}
+
+ {pageContent.luxury_section_subtitle && (
+
+ {pageContent.luxury_section_subtitle}
+ )}
+
+ {pageContent.luxury_section_image && (
+
+
+
+
+
+
+ )}
+ {pageContent.luxury_features && pageContent.luxury_features.length > 0 && (
+
+ {pageContent.luxury_features.map((feature, index) => (
+
+
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Luxury Gallery Section - Dynamic from page content */}
+ {pageContent?.luxury_gallery && Array.isArray(pageContent.luxury_gallery) && pageContent.luxury_gallery.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_gallery_section_title || 'Luxury Gallery'}
+
+ {pageContent.luxury_gallery_section_subtitle && (
+
+ {pageContent.luxury_gallery_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_gallery.map((image, index) => {
+ // Normalize image URL - if it's a relative path, prepend the API URL
+ const imageUrl = image && typeof image === 'string'
+ ? (image.startsWith('http://') || image.startsWith('https://')
+ ? image
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${image.startsWith('/') ? image : '/' + image}`)
+ : '';
+
+ if (!imageUrl) return null;
+
+ return (
+ {
+ const normalizedImages = pageContent.luxury_gallery
+ .map(img => {
+ if (!img || typeof img !== 'string') return null;
+ return img.startsWith('http://') || img.startsWith('https://')
+ ? img
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${img.startsWith('/') ? img : '/' + img}`;
+ })
+ .filter(Boolean) as string[];
+ setLightboxImages(normalizedImages);
+ setLightboxIndex(index);
+ setLightboxOpen(true);
+ }}
+ >
+
{
+ console.error(`Failed to load luxury gallery image: ${imageUrl}`);
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Luxury Testimonials Section - Dynamic from page content */}
+ {pageContent?.luxury_testimonials && pageContent.luxury_testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_testimonials_section_title || 'Guest Experiences'}
+
+ {pageContent.luxury_testimonials_section_subtitle && (
+
+ {pageContent.luxury_testimonials_section_subtitle}
+
+ )}
+
+ Hear from our valued guests about their luxury stay
+
+
+
+ {pageContent.luxury_testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.title && (
+ {testimonial.title}
+ )}
+
+
+
+ "
+ {testimonial.quote}
+
+
+ ))}
+
+
+ )}
+
+ {/* Stats Section - Dynamic from page content */}
+ {pageContent?.stats && Array.isArray(pageContent.stats) && pageContent.stats.length > 0 && (
+
+
+ {/* Decorative elements */}
+
+
+
+
+ {pageContent.stats.map((stat, index) => (
+
+ {stat?.icon && (
+
+ {stat.icon && (LucideIcons as any)[stat.icon] ? (
+ React.createElement((LucideIcons as any)[stat.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ {stat.icon}
+ )}
+
+ )}
+ {stat?.number && (
+
+ {stat.number}
+
+ )}
+ {stat?.label && (
+
+ {stat.label}
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {/* Amenities Section - Dynamic from page content */}
+ {pageContent?.amenities && pageContent.amenities.length > 0 && (
+
+
+
+
+
+
+ {pageContent.amenities_section_title || 'Luxury Amenities'}
+
+
+ {pageContent.amenities_section_subtitle || 'Experience world-class amenities designed for your comfort'}
+
+
+
+ {pageContent.amenities.map((amenity, index) => (
+
+
+ {amenity.image ? (
+
+
+
+ ) : (
+
+ {amenity.icon && (LucideIcons as any)[amenity.icon] ? (
+ React.createElement((LucideIcons as any)[amenity.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {amenity.title}
+
+
+ {amenity.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Testimonials Section - Dynamic from page content */}
+ {pageContent?.testimonials && pageContent.testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.testimonials_section_title || 'Guest Testimonials'}
+
+
+ {pageContent.testimonials_section_subtitle || 'See what our guests say about their experience'}
+
+
+
+ {pageContent.testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.role}
+
+
+
+ {[...Array(5)].map((_, i) => (
+ ★
+ ))}
+
+
+ "
+ "{testimonial.comment}"
+
+
+ ))}
+
+
+ )}
+
+
+ {/* About Preview Section - Dynamic from page content */}
+ {(pageContent?.about_preview_title || pageContent?.about_preview_content) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+
+ {pageContent.about_preview_image && (
+
+
+
+
+ )}
+
+
+
+
+
+ {pageContent.about_preview_title || 'About Our Hotel'}
+
+ {pageContent.about_preview_subtitle && (
+
+ {pageContent.about_preview_subtitle}
+
+ )}
+ {pageContent.about_preview_content && (
+
+ {pageContent.about_preview_content}
+
+ )}
+
+
+ Learn More
+
+
+
+
+
+
+ )}
+
+ {/* Luxury Services Section */}
+ {pageContent?.luxury_services && pageContent.luxury_services.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_services_section_title || 'Luxury Services'}
+
+ {pageContent.luxury_services_section_subtitle && (
+
+ {pageContent.luxury_services_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_services.map((service: any, index: number) => (
+
+
+ {service.image ? (
+
+
+
+ ) : (
+
+ {service.icon && (LucideIcons as any)[service.icon] ? (
+ React.createElement((LucideIcons as any)[service.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {service.title}
+
+
+ {service.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Luxury Experiences Section */}
+ {pageContent?.luxury_experiences && pageContent.luxury_experiences.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_experiences_section_title || 'Unique Experiences'}
+
+ {pageContent.luxury_experiences_section_subtitle && (
+
+ {pageContent.luxury_experiences_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_experiences.map((experience: any, index: number) => (
+
+
+ {experience.image ? (
+
+
+
+ ) : (
+
+ {experience.icon && (LucideIcons as any)[experience.icon] ? (
+ React.createElement((LucideIcons as any)[experience.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {experience.title}
+
+
+ {experience.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Awards Section */}
+ {pageContent?.awards && pageContent.awards.length > 0 && (
+
+
+
+
+
+
+ {pageContent.awards_section_title || 'Awards & Recognition'}
+
+ {pageContent.awards_section_subtitle && (
+
+ {pageContent.awards_section_subtitle}
+
+ )}
+
+
+ {pageContent.awards.map((award: any, index: number) => (
+
+
+ {award.image ? (
+
+
+
+ ) : (
+
+ {award.icon && (LucideIcons as any)[award.icon] ? (
+ React.createElement((LucideIcons as any)[award.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ 🏆
+ )}
+
+ )}
+ {award.year && (
+ {award.year}
+ )}
+
+ {award.title}
+
+ {award.description && (
+
+ {award.description}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* CTA Section */}
+ {(pageContent?.cta_title || pageContent?.cta_subtitle) && (
+
+
+ {pageContent.cta_image && (
+
+
+
+ )}
+
+
+
+
+
+
+ {pageContent.cta_title}
+
+ {pageContent.cta_subtitle && (
+
+ {pageContent.cta_subtitle}
+
+ )}
+ {pageContent.cta_button_text && pageContent.cta_button_link && (
+
+
+ {pageContent.cta_button_text}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Partners Section */}
+ {pageContent?.partners && pageContent.partners.length > 0 && (
+
+
+
+
+
+
+ {pageContent.partners_section_title || 'Our Partners'}
+
+ {pageContent.partners_section_subtitle && (
+
+ {pageContent.partners_section_subtitle}
+
+ )}
+
+
+ {pageContent.partners.map((partner: any, index: number) => (
+
+ {partner.link ? (
+
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+
+ ) : (
+ <>
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+ >
+ )}
+
+ ))}
+
+
+ )}
+
+
+
+
+ {/* Luxury Gallery Lightbox Modal */}
+ {lightboxOpen && lightboxImages.length > 0 && (
+ setLightboxOpen(false)}
+ >
+ {/* Close Button */}
+
+
+ {/* Previous Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Next Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Image Container */}
+ e.stopPropagation()}
+ >
+
+
{
+ console.error(`Failed to load lightbox image: ${lightboxImages[lightboxIndex]}`);
+ }}
+ />
+
+ {/* Image Counter */}
+ {lightboxImages.length > 1 && (
+
+ {lightboxIndex + 1} / {lightboxImages.length}
+
+ )}
+
+ {/* Thumbnail Strip (if more than 1 image) */}
+ {lightboxImages.length > 1 && lightboxImages.length <= 10 && (
+
+ {lightboxImages.map((thumb, idx) => (
+
+ ))}
+
+ )}
-
-
+ )}
>
);
};
diff --git a/Frontend/src/pages/admin/BookingManagementPage.tsx b/Frontend/src/pages/admin/BookingManagementPage.tsx
index 9c7bd13f..ce037b6f 100644
--- a/Frontend/src/pages/admin/BookingManagementPage.tsx
+++ b/Frontend/src/pages/admin/BookingManagementPage.tsx
@@ -1,20 +1,23 @@
import React, { useEffect, useState } from 'react';
-import { Search, Eye, XCircle, CheckCircle, Loader2 } from 'lucide-react';
-import { bookingService, Booking } from '../../services/api';
+import { Search, Eye, XCircle, CheckCircle, Loader2, FileText } from 'lucide-react';
+import { bookingService, Booking, invoiceService } from '../../services/api';
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import Pagination from '../../components/common/Pagination';
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { parseDateLocal } from '../../utils/format';
+import { useNavigate } from 'react-router-dom';
const BookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
+ const navigate = useNavigate();
const [bookings, setBookings] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedBooking, setSelectedBooking] = useState(null);
const [showDetailModal, setShowDetailModal] = useState(false);
const [updatingBookingId, setUpdatingBookingId] = useState(null);
const [cancellingBookingId, setCancellingBookingId] = useState(null);
+ const [creatingInvoice, setCreatingInvoice] = useState(false);
const [filters, setFilters] = useState({
search: '',
status: '',
@@ -80,6 +83,32 @@ const BookingManagementPage: React.FC = () => {
}
};
+ const handleCreateInvoice = async (bookingId: number) => {
+ try {
+ setCreatingInvoice(true);
+ // Ensure bookingId is a number
+ const invoiceData = {
+ booking_id: Number(bookingId),
+ };
+
+ const response = await invoiceService.createInvoice(invoiceData);
+
+ if (response.status === 'success' && response.data?.invoice) {
+ toast.success('Invoice created successfully!');
+ setShowDetailModal(false);
+ navigate(`/admin/invoices/${response.data.invoice.id}`);
+ } else {
+ throw new Error('Failed to create invoice');
+ }
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
+ toast.error(errorMessage);
+ console.error('Invoice creation error:', error);
+ } finally {
+ setCreatingInvoice(false);
+ }
+ };
+
const getStatusBadge = (status: string) => {
const badges: Record = {
pending: {
@@ -622,7 +651,24 @@ const BookingManagementPage: React.FC = () => {
{/* Modal Footer */}
-
+
+
- {/* Search Booking */}
+ {/* Date and Search Filters */}
- 1. Search booking
-
-
-
+
+
+
setBookingNumber(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
- placeholder="Enter booking number"
- className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ type="date"
+ value={selectedDate}
+ onChange={(e) => setSelectedDate(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
-
+
+
+
+ setSearchQuery(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSearchByNumber()}
+ placeholder="Enter booking number"
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+
+
- {/* Booking Info */}
+ {/* Check-ins and Check-outs Lists */}
+ {!booking && (
+
+ {/* Check-ins for Today */}
+
+
+
+
+ Check-ins for {formatDate(selectedDate)}
+
+
+ {checkInBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkInBookings.length === 0 ? (
+
+
+ No check-ins scheduled for this date
+
+ ) : (
+
+ {checkInBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Check-outs for Today */}
+
+
+
+
+ Check-outs for {formatDate(selectedDate)}
+
+
+ {checkOutBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkOutBookings.length === 0 ? (
+
+
+ No check-outs scheduled for this date
+
+ ) : (
+
+ {checkOutBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-orange-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Booking Info and Check-in Form */}
{booking && (
<>
-
-
- 2. Booking Information
-
+
+
+
+ 2. Booking Information
+
+
+
@@ -299,7 +541,6 @@ const CheckInPage: React.FC = () => {
{(() => {
- // Use payment_balance from API if available, otherwise calculate from payments
const paymentBalance = booking.payment_balance || (() => {
const completedPayments = booking.payments?.filter(
(p) => p.payment_status === 'completed'
@@ -545,19 +786,6 @@ const CheckInPage: React.FC = () => {
>
)}
-
- {/* Empty State */}
- {!booking && !searching && (
-
-
-
- No booking selected
-
-
- Please enter booking number above to start check-in process
-
-
- )}
);
};
diff --git a/Frontend/src/pages/admin/InvoiceManagementPage.tsx b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
index 3d897a7a..e7d6245d 100644
--- a/Frontend/src/pages/admin/InvoiceManagementPage.tsx
+++ b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
@@ -47,7 +47,8 @@ const InvoiceManagementPage: React.FC = () => {
invoiceList = invoiceList.filter((inv) =>
inv.invoice_number.toLowerCase().includes(filters.search.toLowerCase()) ||
inv.customer_name.toLowerCase().includes(filters.search.toLowerCase()) ||
- inv.customer_email.toLowerCase().includes(filters.search.toLowerCase())
+ inv.customer_email.toLowerCase().includes(filters.search.toLowerCase()) ||
+ (inv.promotion_code && inv.promotion_code.toLowerCase().includes(filters.search.toLowerCase()))
);
}
@@ -130,11 +131,11 @@ const InvoiceManagementPage: React.FC = () => {
Manage and track all invoices
@@ -190,6 +191,9 @@ const InvoiceManagementPage: React.FC = () => {
Amount
+
+ Promotion
+
Status
@@ -235,6 +239,25 @@ const InvoiceManagementPage: React.FC = () => {
Due: {formatCurrency(invoice.balance_due)}
)}
+ {invoice.discount_amount > 0 && (
+
+ Discount: -{formatCurrency(invoice.discount_amount)}
+
+ )}
+
+
+ {invoice.promotion_code ? (
+
+ {invoice.promotion_code}
+
+ ) : (
+ —
+ )}
+ {invoice.is_proforma && (
+
+ Proforma
+
+ )}
@@ -274,7 +297,7 @@ const InvoiceManagementPage: React.FC = () => {
})
) : (
-
+
No invoices found
diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx
index d52089e8..d33edbec 100644
--- a/Frontend/src/pages/admin/PageContentDashboard.tsx
+++ b/Frontend/src/pages/admin/PageContentDashboard.tsx
@@ -7,13 +7,6 @@ import {
Search,
Save,
Globe,
- Facebook,
- Twitter,
- Instagram,
- Linkedin,
- Youtube,
- MapPin,
- Phone,
X,
Plus,
Trash2,
@@ -31,6 +24,8 @@ import { pageContentService, PageContent, PageType, UpdatePageContentData, banne
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import { ConfirmationDialog } from '../../components/common';
+import IconPicker from '../../components/admin/IconPicker';
+import { luxuryContentSeed } from '../../data/luxuryContentSeed';
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo';
@@ -62,7 +57,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -113,6 +108,21 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Helper function to normalize arrays (handle both arrays and JSON strings)
+ const normalizeArray = (value: any): any[] => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ if (typeof value === 'string') {
+ try {
+ const parsed = JSON.parse(value);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+ }
+ return [];
+ };
+
const initializeFormData = (contents: Record) => {
// Home
if (contents.home) {
@@ -130,6 +140,45 @@ const PageContentDashboard: React.FC = () => {
og_title: contents.home.og_title || '',
og_description: contents.home.og_description || '',
og_image: contents.home.og_image || '',
+ features: normalizeArray(contents.home.features),
+ amenities_section_title: contents.home.amenities_section_title || '',
+ amenities_section_subtitle: contents.home.amenities_section_subtitle || '',
+ amenities: normalizeArray(contents.home.amenities),
+ testimonials_section_title: contents.home.testimonials_section_title || '',
+ testimonials_section_subtitle: contents.home.testimonials_section_subtitle || '',
+ testimonials: normalizeArray(contents.home.testimonials),
+ about_preview_title: contents.home.about_preview_title || '',
+ about_preview_subtitle: contents.home.about_preview_subtitle || '',
+ about_preview_content: contents.home.about_preview_content || '',
+ about_preview_image: contents.home.about_preview_image || '',
+ stats: normalizeArray(contents.home.stats),
+ luxury_section_title: contents.home.luxury_section_title || '',
+ luxury_section_subtitle: contents.home.luxury_section_subtitle || '',
+ luxury_section_image: contents.home.luxury_section_image || '',
+ luxury_features: normalizeArray(contents.home.luxury_features),
+ luxury_gallery_section_title: contents.home.luxury_gallery_section_title || '',
+ luxury_gallery_section_subtitle: contents.home.luxury_gallery_section_subtitle || '',
+ luxury_gallery: normalizeArray(contents.home.luxury_gallery),
+ luxury_testimonials_section_title: contents.home.luxury_testimonials_section_title || '',
+ luxury_testimonials_section_subtitle: contents.home.luxury_testimonials_section_subtitle || '',
+ luxury_testimonials: normalizeArray(contents.home.luxury_testimonials),
+ luxury_services_section_title: contents.home.luxury_services_section_title || '',
+ luxury_services_section_subtitle: contents.home.luxury_services_section_subtitle || '',
+ luxury_services: normalizeArray(contents.home.luxury_services),
+ luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
+ luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
+ luxury_experiences: normalizeArray(contents.home.luxury_experiences),
+ awards_section_title: contents.home.awards_section_title || '',
+ awards_section_subtitle: contents.home.awards_section_subtitle || '',
+ awards: normalizeArray(contents.home.awards),
+ cta_title: contents.home.cta_title || '',
+ cta_subtitle: contents.home.cta_subtitle || '',
+ cta_button_text: contents.home.cta_button_text || '',
+ cta_button_link: contents.home.cta_button_link || '',
+ cta_image: contents.home.cta_image || '',
+ partners_section_title: contents.home.partners_section_title || '',
+ partners_section_subtitle: contents.home.partners_section_subtitle || '',
+ partners: normalizeArray(contents.home.partners),
});
}
@@ -154,8 +203,14 @@ const PageContentDashboard: React.FC = () => {
description: contents.about.description || '',
content: contents.about.content || '',
story_content: contents.about.story_content || '',
- values: contents.about.values || [],
- features: contents.about.features || [],
+ values: normalizeArray(contents.about.values),
+ features: normalizeArray(contents.about.features),
+ about_hero_image: contents.about.about_hero_image || '',
+ mission: contents.about.mission || '',
+ vision: contents.about.vision || '',
+ team: normalizeArray(contents.about.team),
+ timeline: normalizeArray(contents.about.timeline),
+ achievements: normalizeArray(contents.about.achievements),
meta_title: contents.about.meta_title || '',
meta_description: contents.about.meta_description || '',
});
@@ -169,6 +224,7 @@ const PageContentDashboard: React.FC = () => {
social_links: contents.footer.social_links || {},
footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] },
badges: contents.footer.badges || [],
+ copyright_text: contents.footer.copyright_text || '',
meta_title: contents.footer.meta_title || '',
meta_description: contents.footer.meta_description || '',
});
@@ -244,7 +300,7 @@ const PageContentDashboard: React.FC = () => {
try {
setUploadingImage(true);
const response = await bannerService.uploadBannerImage(file);
- if (response.status === 'success' || response.success) {
+ if (response.success) {
setBannerFormData({ ...bannerFormData, image_url: response.data.image_url });
toast.success('Image uploaded successfully');
}
@@ -257,6 +313,27 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Generic image upload handler for page content images
+ const handlePageContentImageUpload = async (
+ file: File,
+ onSuccess: (imageUrl: string) => void
+ ) => {
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error('Image size must be less than 5MB');
+ return;
+ }
+
+ try {
+ const response = await pageContentService.uploadImage(file);
+ if (response.success) {
+ onSuccess(response.data.image_url);
+ toast.success('Image uploaded successfully');
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to upload image');
+ }
+ };
+
const handleBannerSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -270,7 +347,7 @@ const PageContentDashboard: React.FC = () => {
if (imageFile && !imageUrl) {
setUploadingImage(true);
const uploadResponse = await bannerService.uploadBannerImage(imageFile);
- if (uploadResponse.status === 'success' || uploadResponse.success) {
+ if (uploadResponse.success) {
imageUrl = uploadResponse.data.image_url;
} else {
throw new Error('Failed to upload image');
@@ -305,9 +382,9 @@ const PageContentDashboard: React.FC = () => {
setEditingBanner(banner);
setBannerFormData({
title: banner.title || '',
- description: '',
+ description: banner.description || '',
image_url: banner.image_url || '',
- link: banner.link || '',
+ link_url: banner.link_url || '',
position: banner.position || 'home',
display_order: banner.display_order || 0,
is_active: banner.is_active ?? true,
@@ -343,7 +420,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -531,6 +608,42 @@ const PageContentDashboard: React.FC = () => {
{/* Home Tab */}
{activeTab === 'home' && (
+ {/* Seed Data Button */}
+
+
+
+ Quick Start
+ Load pre-configured luxury content to get started quickly
+
+
+
+
+
{/* Home Page Content Section */}
Home Page Content
@@ -559,14 +672,38 @@ const PageContentDashboard: React.FC = () => {
-
- setHomeData({ ...homeData, hero_image: e.target.value })}
- className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
- placeholder="https://example.com/hero-image.jpg"
- />
+
+
+ setHomeData({ ...homeData, hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {homeData.hero_image && (
+
+
+
+ )}
@@ -621,17 +758,1388 @@ const PageContentDashboard: React.FC = () => {
placeholder="SEO Meta Description"
/>
+
+
-
-
+ {/* Amenities Section */}
+
+ Amenities Section
+
+
+
+ setHomeData({ ...homeData, amenities_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Amenities"
+ />
+
+
+ setHomeData({ ...homeData, amenities_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience world-class amenities"
+ />
+
+
+
+ Amenities
+
+
+
+ {Array.isArray(homeData.amenities) && homeData.amenities.map((amenity, index) => (
+
+
+ Amenity {index + 1}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], icon: iconName };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], image: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {amenity?.image && (
+
+
+
+ )}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], title: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.amenities || homeData.amenities.length === 0) && (
+ No amenities added yet. Click "Add Amenity" to get started.
+ )}
+
+
+
+ {/* Luxury Section */}
+
+ Luxury Section
+
+
+
+ setHomeData({ ...homeData, luxury_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience Unparalleled Luxury"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Where elegance meets comfort in every detail"
+ />
+
+
+
+
+ setHomeData({ ...homeData, luxury_section_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/luxury-image.jpg or upload"
+ />
+
+
+ {homeData.luxury_section_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Luxury Features Section */}
+
+
+ Luxury Features
+
+
+
+ {Array.isArray(homeData.luxury_features) && homeData.luxury_features.map((feature, index) => (
+
+
+ Luxury Feature {index + 1}
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], icon: iconName };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], title: e.target.value };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Feature Title"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_features || homeData.luxury_features.length === 0) && (
+ No luxury features added yet. Click "Add Luxury Feature" to get started.
+ )}
+
+
+
+ {/* Luxury Gallery Section */}
+
+
+ Luxury Gallery
+
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Gallery"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Discover our exquisite spaces and amenities"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_gallery) && homeData.luxury_gallery.map((image, index) => (
+
+ {
+ setHomeData((prevData) => {
+ const currentGallery = Array.isArray(prevData.luxury_gallery) ? [...prevData.luxury_gallery] : [];
+ currentGallery[index] = e.target.value;
+ return { ...prevData, luxury_gallery: currentGallery };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {image && (
+
+
+
+ )}
+
+ ))}
+ {(!homeData.luxury_gallery || homeData.luxury_gallery.length === 0) && (
+ No gallery images added yet. Click "Add Gallery Image" to get started.
+ )}
+
+
+
+ {/* Luxury Testimonials Section */}
+
+
+ Luxury Testimonials
+
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Guest Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Hear from our valued guests about their luxury stay"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_testimonials) && homeData.luxury_testimonials.map((testimonial, index) => (
+
+
+ Luxury Testimonial {index + 1}
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], name: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], title: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], image: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {testimonial?.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_testimonials || homeData.luxury_testimonials.length === 0) && (
+ No luxury testimonials added yet. Click "Add Luxury Testimonial" to get started.
+ )}
+
+
+
+ {/* About Preview Section */}
+
+ About Preview Section
+
+
+
+ setHomeData({ ...homeData, about_preview_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="About Our Hotel"
+ />
+
+
+
+ setHomeData({ ...homeData, about_preview_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ />
+
+
+
+
+
+
+
+ setHomeData({ ...homeData, about_preview_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/about-image.jpg or upload"
+ />
+
+
+ {homeData.about_preview_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Stats Section */}
+
+
+ Statistics Section
+
+
+
+ {Array.isArray(homeData.stats) && homeData.stats.map((stat, index) => (
+
+
+ Stat {index + 1}
+
+
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], number: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="1000+"
+ />
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], label: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Happy Guests"
+ />
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], icon: iconName };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ label="Icon"
+ />
+
+
+
+ ))}
+ {(!homeData.stats || homeData.stats.length === 0) && (
+ No statistics added yet. Click "Add Stat" to get started.
+ )}
+
+
+
+ {/* Luxury Services Section */}
+
+
+ Luxury Services
+
+
+
+ {Array.isArray(homeData.luxury_services) && homeData.luxury_services.map((service, index) => (
+
+
+ Service {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {service?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Service Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_services || homeData.luxury_services.length === 0) && (
+ No services added yet. Click "Add Service" to get started.
+ )}
+
+
+
+ {/* Luxury Experiences Section */}
+
+
+ Luxury Experiences
+
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unique Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unforgettable moments await"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_experiences) && homeData.luxury_experiences.map((experience, index) => (
+
+
+ Experience {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {experience?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Experience Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_experiences || homeData.luxury_experiences.length === 0) && (
+ No experiences added yet. Click "Add Experience" to get started.
+ )}
+
+
+
+ {/* Awards Section */}
+
+
+ Awards & Certifications
+
+
+
+
+ setHomeData({ ...homeData, awards_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Awards & Recognition"
+ />
+
+
+
+ setHomeData({ ...homeData, awards_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Recognized excellence in hospitality"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.awards) && homeData.awards.map((award, index) => (
+
+
+ Award {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], year: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="2024"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {award?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Award Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.awards || homeData.awards.length === 0) && (
+ No awards added yet. Click "Add Award" to get started.
+ )}
+
+
+
+ {/* CTA Section */}
+
+ Call to Action Section
+
+
+
+ setHomeData({ ...homeData, cta_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Ready to Experience Luxury?"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book your stay today"
+ />
+
+
+
+
+ setHomeData({ ...homeData, cta_button_text: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book Now"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_button_link: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="/rooms"
+ />
+
+
+
+
+
+ setHomeData({ ...homeData, cta_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/cta-image.jpg or upload"
+ />
+
+
+ {homeData.cta_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Partners Section */}
+
+
+ Partners & Brands
+
+
+
+
+ setHomeData({ ...homeData, partners_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Our Partners"
+ />
+
+
+
+ setHomeData({ ...homeData, partners_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Trusted by leading brands"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.partners) && homeData.partners.map((partner, index) => (
+
+
+ Partner {index + 1}
+
+
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], name: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Partner Name"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], logo: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com/logo.png"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], link: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com"
+ />
+
+
+
+ ))}
+ {(!homeData.partners || homeData.partners.length === 0) && (
+ No partners added yet. Click "Add Partner" to get started.
+ )}
+
+
+
+ {/* Save Button */}
+
+
+
@@ -696,8 +2204,11 @@ const PageContentDashboard: React.FC = () => {
{banner.title}
- {banner.link && (
- {banner.link}
+ {banner.description && (
+ {banner.description}
+ )}
+ {banner.link_url && (
+ {banner.link_url}
)}
@@ -781,12 +2292,24 @@ const PageContentDashboard: React.FC = () => {
/>
+
+
+
+
setBannerFormData({ ...bannerFormData, link: e.target.value })}
+ value={bannerFormData.link_url}
+ onChange={(e) => setBannerFormData({ ...bannerFormData, link_url: e.target.value })}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
placeholder="https://example.com"
/>
@@ -1140,6 +2663,690 @@ const PageContentDashboard: React.FC = () => {
/>
+ {/* Hero Image */}
+
+
+
+ setAboutData({ ...aboutData, about_hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {aboutData.about_hero_image && (
+
+
+
+ )}
+
+
+ {/* Mission */}
+
+
+
+
+ {/* Vision */}
+
+
+
+
+ {/* Values Section */}
+
+
+ Our Values
+
+
+ {Array.isArray(aboutData.values) && aboutData.values.length > 0 ? (
+
+ {aboutData.values.map((value, index) => (
+
+
+ Value {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], icon };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], title: e.target.value };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Value title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No values added yet. Click "Add Value" to get started.
+ )}
+
+
+ {/* Features Section */}
+
+
+ Features
+
+
+ {Array.isArray(aboutData.features) && aboutData.features.length > 0 ? (
+
+ {aboutData.features.map((feature, index) => (
+
+
+ Feature {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], icon };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], title: e.target.value };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Feature title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No features added yet. Click "Add Feature" to get started.
+ )}
+
+
+ {/* Team Section */}
+
+
+ Team Members
+
+
+ {Array.isArray(aboutData.team) && aboutData.team.length > 0 ? (
+
+ {aboutData.team.map((member, index) => (
+
+
+ Team Member {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], name: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Full name"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], role: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Job title"
+ />
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], image: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {member.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+
+ ) : (
+ No team members added yet. Click "Add Team Member" to get started.
+ )}
+
+
+ {/* Timeline Section */}
+
+
+ Timeline / History
+
+
+ {Array.isArray(aboutData.timeline) && aboutData.timeline.length > 0 ? (
+
+ {aboutData.timeline.map((event, index) => (
+
+
+ Event {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], year: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], title: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Event title"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], image: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {event.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No timeline events added yet. Click "Add Event" to get started.
+ )}
+
+
+ {/* Achievements Section */}
+
+
+ Achievements & Awards
+
+
+ {Array.isArray(aboutData.achievements) && aboutData.achievements.length > 0 ? (
+
+ {aboutData.achievements.map((achievement, index) => (
+
+
+ Achievement {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], icon };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ />
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], title: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Achievement title"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], year: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], image: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {achievement.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No achievements added yet. Click "Add Achievement" to get started.
+ )}
+
+
@@ -1400,6 +3607,28 @@ const PageContentDashboard: React.FC = () => {
+ {/* Copyright Text */}
+
+ Copyright Text
+
+
+
+ setFooterData({ ...footerData, copyright_text: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200"
+ placeholder="© {YEAR} Luxury Hotel. All rights reserved."
+ />
+
+ The {`{YEAR}`} placeholder will be automatically replaced with the current year.
+
+
+
+
+
Our Values
- ++ {value.title} +
++ {value.description} +
- {value.title} -
-- {value.description} -
+
+
+
+
+
+
+ Excellence Defined
+
+
Why Choose Us
-
+
+
+
+
+
-
- {features.map((feature, index) => (
-
-
-
+
+ {features.map((feature, index) => {
+ const FeatureIcon = getIconComponent(feature.icon);
+ return (
+
+
+
+
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
-
- {feature.title}
-
-
- {feature.description}
-
-
- ))}
+ );
+ })}
+ {/* Mission & Vision Section */}
+ {(pageContent?.mission || pageContent?.vision) && (
+
+
+
+
+
+
+
+ {pageContent.mission && (
+
+
+
+
+
+ Our Mission
+
+ {pageContent.mission}
+
+
+ )}
+ {pageContent.vision && (
+
+
+
+
+
+ Our Vision
+
+ {pageContent.vision}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Team Section */}
+ {team && team.length > 0 && (
+
+
+
+
+
+ Meet The Experts
+
+
+ Our Team
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Timeline Section */}
+ {timeline && timeline.length > 0 && (
+
+
+
+
+
+
+ Our Journey
+
+
+ Our History
+
+
+
+
+
+
+
+
+
+
+ {timeline.map((event: any, index: number) => (
+
+
+
+
+
+ {event.year}
+
+
+ {event.title}
+ {event.description}
+ {event.image && (
+
+
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Achievements Section */}
+ {achievements && achievements.length > 0 && (
+
+
+
+
+
+
+ Recognition
+
+
+ Achievements & Awards
+
+
+
+
+
+
+
+
+ {achievements.map((achievement: any, index: number) => {
+ const AchievementIcon = getIconComponent(achievement.icon);
+ return (
+
+
+
+
+
+
+
+ {achievement.year && (
+ {achievement.year}
+ )}
+
+ {achievement.title}
+ {achievement.description}
+ {achievement.image && (
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ )}
+
{/* Contact Section */}
-
-
-
-
-
+
+
+
+
+
+
+ Connect With Us
+
+
Get In Touch
-
-
+
+
+
+
+
+
We'd love to hear from you. Contact us for reservations or inquiries.
-
-
-
-
+
+
+
+
-
+
Address
-
+
{displayAddress
.split('\n').map((line, i) => (
@@ -269,40 +587,41 @@ const AboutPage: React.FC = () => {
))}
-
-
-
+
-
-
-
+
-
+
- Explore Our Rooms
-
+ Explore Our Rooms
+
+
diff --git a/Frontend/src/pages/ContactPage.tsx b/Frontend/src/pages/ContactPage.tsx
index 32668000..619729da 100644
--- a/Frontend/src/pages/ContactPage.tsx
+++ b/Frontend/src/pages/ContactPage.tsx
@@ -99,7 +99,7 @@ const ContactPage: React.FC = () => {
useEffect(() => {
const fetchPageContent = async () => {
try {
- const response = await pageContentService.getPageContent('contact');
+ const response = await pageContentService.getContactContent();
if (response.status === 'success' && response.data?.page_content) {
setPageContent(response.data.page_content);
diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx
index 723abf87..15ff2648 100644
--- a/Frontend/src/pages/HomePage.tsx
+++ b/Frontend/src/pages/HomePage.tsx
@@ -3,7 +3,13 @@ import { Link } from 'react-router-dom';
import {
ArrowRight,
AlertCircle,
+ Star,
+ X,
+ ChevronLeft,
+ ChevronRight,
+ ZoomIn,
} from 'lucide-react';
+import * as LucideIcons from 'lucide-react';
import {
BannerCarousel,
BannerSkeleton,
@@ -32,6 +38,33 @@ const HomePage: React.FC = () => {
const [isLoadingNewest, setIsLoadingNewest] = useState(true);
const [isLoadingContent, setIsLoadingContent] = useState(true);
const [error, setError] = useState(null);
+ const [lightboxOpen, setLightboxOpen] = useState(false);
+ const [lightboxIndex, setLightboxIndex] = useState(0);
+ const [lightboxImages, setLightboxImages] = useState([]);
+
+ // Handle keyboard navigation for lightbox
+ useEffect(() => {
+ if (!lightboxOpen) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setLightboxOpen(false);
+ } else if (e.key === 'ArrowLeft' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === 0 ? lightboxImages.length - 1 : prev - 1));
+ } else if (e.key === 'ArrowRight' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === lightboxImages.length - 1 ? 0 : prev + 1));
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ // Prevent body scroll when lightbox is open
+ document.body.style.overflow = 'hidden';
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = 'unset';
+ };
+ }, [lightboxOpen, lightboxImages.length]);
// Combine featured and newest rooms, removing duplicates
const combinedRooms = useMemo(() => {
@@ -57,22 +90,118 @@ const HomePage: React.FC = () => {
const fetchPageContent = async () => {
try {
setIsLoadingContent(true);
- const response = await pageContentService.getPageContent('home');
+ const response = await pageContentService.getHomeContent();
if (response.status === 'success' && response.data?.page_content) {
- setPageContent(response.data.page_content);
+ const content = response.data.page_content;
+
+ // Parse JSON fields if they come as strings (backward compatibility)
+ if (typeof content.features === 'string') {
+ try {
+ content.features = JSON.parse(content.features);
+ } catch (e) {
+ content.features = [];
+ }
+ }
+ if (typeof content.amenities === 'string') {
+ try {
+ content.amenities = JSON.parse(content.amenities);
+ } catch (e) {
+ content.amenities = [];
+ }
+ }
+ if (typeof content.testimonials === 'string') {
+ try {
+ content.testimonials = JSON.parse(content.testimonials);
+ } catch (e) {
+ content.testimonials = [];
+ }
+ }
+ if (typeof content.gallery_images === 'string') {
+ try {
+ content.gallery_images = JSON.parse(content.gallery_images);
+ } catch (e) {
+ content.gallery_images = [];
+ }
+ }
+ if (typeof content.stats === 'string') {
+ try {
+ content.stats = JSON.parse(content.stats);
+ } catch (e) {
+ content.stats = [];
+ }
+ }
+ // Parse luxury fields
+ if (typeof content.luxury_features === 'string') {
+ try {
+ content.luxury_features = JSON.parse(content.luxury_features);
+ } catch (e) {
+ content.luxury_features = [];
+ }
+ }
+ if (typeof content.luxury_gallery === 'string') {
+ try {
+ const parsed = JSON.parse(content.luxury_gallery);
+ content.luxury_gallery = Array.isArray(parsed) ? parsed.filter(img => img && typeof img === 'string' && img.trim() !== '') : [];
+ } catch (e) {
+ content.luxury_gallery = [];
+ }
+ }
+ // Ensure luxury_gallery is an array and filter out empty values
+ if (Array.isArray(content.luxury_gallery)) {
+ content.luxury_gallery = content.luxury_gallery.filter(img => img && typeof img === 'string' && img.trim() !== '');
+ } else {
+ content.luxury_gallery = [];
+ }
+ if (typeof content.luxury_testimonials === 'string') {
+ try {
+ content.luxury_testimonials = JSON.parse(content.luxury_testimonials);
+ } catch (e) {
+ content.luxury_testimonials = [];
+ }
+ }
+ if (typeof content.luxury_services === 'string') {
+ try {
+ content.luxury_services = JSON.parse(content.luxury_services);
+ } catch (e) {
+ content.luxury_services = [];
+ }
+ }
+ if (typeof content.luxury_experiences === 'string') {
+ try {
+ content.luxury_experiences = JSON.parse(content.luxury_experiences);
+ } catch (e) {
+ content.luxury_experiences = [];
+ }
+ }
+ if (typeof content.awards === 'string') {
+ try {
+ content.awards = JSON.parse(content.awards);
+ } catch (e) {
+ content.awards = [];
+ }
+ }
+ if (typeof content.partners === 'string') {
+ try {
+ content.partners = JSON.parse(content.partners);
+ } catch (e) {
+ content.partners = [];
+ }
+ }
+
+ setPageContent(content);
// Update document title and meta tags
- if (response.data.page_content.meta_title) {
- document.title = response.data.page_content.meta_title;
+ if (content.meta_title) {
+ document.title = content.meta_title;
}
- if (response.data.page_content.meta_description) {
+ if (content.meta_description) {
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
- metaDescription.setAttribute('content', response.data.page_content.meta_description);
+ metaDescription.setAttribute('content', content.meta_description);
}
}
} catch (err: any) {
@@ -215,27 +344,34 @@ const HomePage: React.FC = () => {
)}
-
+
+ {/* Subtle background pattern */}
+
+
{/* Featured & Newest Rooms Section - Combined Carousel */}
-
+
{/* Section Header - Centered */}
-
+
+
+
+
{pageContent?.hero_title || 'Featured & Newest Rooms'}
-
+
{pageContent?.hero_subtitle || pageContent?.description || 'Discover our most popular accommodations and latest additions'}
{/* View All Rooms Button - Golden, Centered */}
-
+
+
View All Rooms
-
+
@@ -300,79 +436,818 @@ const HomePage: React.FC = () => {
)}
- {/* Features Section */}
-
-
- {/* Decorative gold accent */}
-
-
-
-
-
- 🏨
-
-
- Easy Booking
-
-
- Search and book rooms with just a few clicks
-
-
+ {/* Features Section - Dynamic from page content */}
+ {(() => {
+ // Filter out empty features (no title or description)
+ const validFeatures = pageContent?.features?.filter(
+ (f: any) => f && (f.title || f.description)
+ ) || [];
+
+ return (validFeatures.length > 0 || !pageContent) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+ {/* Subtle background pattern */}
+
+
+
+ {validFeatures.length > 0 ? (
+ validFeatures.map((feature: any, index: number) => (
+
+ {feature.image ? (
+
+
+
+ ) : (
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+ {feature.title && (
+
+ {feature.title}
+
+ )}
+ {feature.description && (
+
+ {feature.description}
+
+ )}
+
+ ))
+ ) : (
+ <>
+
+
+ 🏨
+
+
+ Easy Booking
+
+
+ Search and book rooms with just a few clicks
+
+
-
-
- 💰
-
-
- Best Prices
-
-
- Best price guarantee in the market
-
-
+
+
+ 💰
+
+
+ Best Prices
+
+
+ Best price guarantee in the market
+
+
-
-
- 🎧
+
+
+ 🎧
+
+
+ 24/7 Support
+
+
+ Support team always ready to serve
+
+
+ >
+ )}
-
- 24/7 Support
-
-
- Support team always ready to serve
+
+
+ );
+ })()}
+
+ {/* Luxury Section - Dynamic from page content */}
+ {(pageContent?.luxury_section_title || (pageContent?.luxury_features && pageContent.luxury_features.length > 0)) && (
+
+
+
+
+
+
+ {pageContent.luxury_section_title || 'Experience Unparalleled Luxury'}
+
+ {pageContent.luxury_section_subtitle && (
+
+ {pageContent.luxury_section_subtitle}
+ )}
+
+ {pageContent.luxury_section_image && (
+
+
+
+
+
+
+ )}
+ {pageContent.luxury_features && pageContent.luxury_features.length > 0 && (
+
+ {pageContent.luxury_features.map((feature, index) => (
+
+
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Luxury Gallery Section - Dynamic from page content */}
+ {pageContent?.luxury_gallery && Array.isArray(pageContent.luxury_gallery) && pageContent.luxury_gallery.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_gallery_section_title || 'Luxury Gallery'}
+
+ {pageContent.luxury_gallery_section_subtitle && (
+
+ {pageContent.luxury_gallery_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_gallery.map((image, index) => {
+ // Normalize image URL - if it's a relative path, prepend the API URL
+ const imageUrl = image && typeof image === 'string'
+ ? (image.startsWith('http://') || image.startsWith('https://')
+ ? image
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${image.startsWith('/') ? image : '/' + image}`)
+ : '';
+
+ if (!imageUrl) return null;
+
+ return (
+ {
+ const normalizedImages = pageContent.luxury_gallery
+ .map(img => {
+ if (!img || typeof img !== 'string') return null;
+ return img.startsWith('http://') || img.startsWith('https://')
+ ? img
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${img.startsWith('/') ? img : '/' + img}`;
+ })
+ .filter(Boolean) as string[];
+ setLightboxImages(normalizedImages);
+ setLightboxIndex(index);
+ setLightboxOpen(true);
+ }}
+ >
+
{
+ console.error(`Failed to load luxury gallery image: ${imageUrl}`);
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Luxury Testimonials Section - Dynamic from page content */}
+ {pageContent?.luxury_testimonials && pageContent.luxury_testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_testimonials_section_title || 'Guest Experiences'}
+
+ {pageContent.luxury_testimonials_section_subtitle && (
+
+ {pageContent.luxury_testimonials_section_subtitle}
+
+ )}
+
+ Hear from our valued guests about their luxury stay
+
+
+
+ {pageContent.luxury_testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.title && (
+ {testimonial.title}
+ )}
+
+
+
+ "
+ {testimonial.quote}
+
+
+ ))}
+
+
+ )}
+
+ {/* Stats Section - Dynamic from page content */}
+ {pageContent?.stats && Array.isArray(pageContent.stats) && pageContent.stats.length > 0 && (
+
+
+ {/* Decorative elements */}
+
+
+
+
+ {pageContent.stats.map((stat, index) => (
+
+ {stat?.icon && (
+
+ {stat.icon && (LucideIcons as any)[stat.icon] ? (
+ React.createElement((LucideIcons as any)[stat.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ {stat.icon}
+ )}
+
+ )}
+ {stat?.number && (
+
+ {stat.number}
+
+ )}
+ {stat?.label && (
+
+ {stat.label}
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {/* Amenities Section - Dynamic from page content */}
+ {pageContent?.amenities && pageContent.amenities.length > 0 && (
+
+
+
+
+
+
+ {pageContent.amenities_section_title || 'Luxury Amenities'}
+
+
+ {pageContent.amenities_section_subtitle || 'Experience world-class amenities designed for your comfort'}
+
+
+
+ {pageContent.amenities.map((amenity, index) => (
+
+
+ {amenity.image ? (
+
+
+
+ ) : (
+
+ {amenity.icon && (LucideIcons as any)[amenity.icon] ? (
+ React.createElement((LucideIcons as any)[amenity.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {amenity.title}
+
+
+ {amenity.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Testimonials Section - Dynamic from page content */}
+ {pageContent?.testimonials && pageContent.testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.testimonials_section_title || 'Guest Testimonials'}
+
+
+ {pageContent.testimonials_section_subtitle || 'See what our guests say about their experience'}
+
+
+
+ {pageContent.testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.role}
+
+
+
+ {[...Array(5)].map((_, i) => (
+ ★
+ ))}
+
+
+ "
+ "{testimonial.comment}"
+
+
+ ))}
+
+
+ )}
+
+
+ {/* About Preview Section - Dynamic from page content */}
+ {(pageContent?.about_preview_title || pageContent?.about_preview_content) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+
+ {pageContent.about_preview_image && (
+
+
+
+
+ )}
+
+
+
+
+
+ {pageContent.about_preview_title || 'About Our Hotel'}
+
+ {pageContent.about_preview_subtitle && (
+
+ {pageContent.about_preview_subtitle}
+
+ )}
+ {pageContent.about_preview_content && (
+
+ {pageContent.about_preview_content}
+
+ )}
+
+
+ Learn More
+
+
+
+
+
+
+ )}
+
+ {/* Luxury Services Section */}
+ {pageContent?.luxury_services && pageContent.luxury_services.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_services_section_title || 'Luxury Services'}
+
+ {pageContent.luxury_services_section_subtitle && (
+
+ {pageContent.luxury_services_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_services.map((service: any, index: number) => (
+
+
+ {service.image ? (
+
+
+
+ ) : (
+
+ {service.icon && (LucideIcons as any)[service.icon] ? (
+ React.createElement((LucideIcons as any)[service.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {service.title}
+
+
+ {service.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Luxury Experiences Section */}
+ {pageContent?.luxury_experiences && pageContent.luxury_experiences.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_experiences_section_title || 'Unique Experiences'}
+
+ {pageContent.luxury_experiences_section_subtitle && (
+
+ {pageContent.luxury_experiences_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_experiences.map((experience: any, index: number) => (
+
+
+ {experience.image ? (
+
+
+
+ ) : (
+
+ {experience.icon && (LucideIcons as any)[experience.icon] ? (
+ React.createElement((LucideIcons as any)[experience.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {experience.title}
+
+
+ {experience.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Awards Section */}
+ {pageContent?.awards && pageContent.awards.length > 0 && (
+
+
+
+
+
+
+ {pageContent.awards_section_title || 'Awards & Recognition'}
+
+ {pageContent.awards_section_subtitle && (
+
+ {pageContent.awards_section_subtitle}
+
+ )}
+
+
+ {pageContent.awards.map((award: any, index: number) => (
+
+
+ {award.image ? (
+
+
+
+ ) : (
+
+ {award.icon && (LucideIcons as any)[award.icon] ? (
+ React.createElement((LucideIcons as any)[award.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ 🏆
+ )}
+
+ )}
+ {award.year && (
+ {award.year}
+ )}
+
+ {award.title}
+
+ {award.description && (
+
+ {award.description}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* CTA Section */}
+ {(pageContent?.cta_title || pageContent?.cta_subtitle) && (
+
+
+ {pageContent.cta_image && (
+
+
+
+ )}
+
+
+
+
+
+
+ {pageContent.cta_title}
+
+ {pageContent.cta_subtitle && (
+
+ {pageContent.cta_subtitle}
+
+ )}
+ {pageContent.cta_button_text && pageContent.cta_button_link && (
+
+
+ {pageContent.cta_button_text}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Partners Section */}
+ {pageContent?.partners && pageContent.partners.length > 0 && (
+
+
+
+
+
+
+ {pageContent.partners_section_title || 'Our Partners'}
+
+ {pageContent.partners_section_subtitle && (
+
+ {pageContent.partners_section_subtitle}
+
+ )}
+
+
+ {pageContent.partners.map((partner: any, index: number) => (
+
+ {partner.link ? (
+
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+
+ ) : (
+ <>
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+ >
+ )}
+
+ ))}
+
+
+ )}
+
+
+
+
+ {/* Luxury Gallery Lightbox Modal */}
+ {lightboxOpen && lightboxImages.length > 0 && (
+ setLightboxOpen(false)}
+ >
+ {/* Close Button */}
+
+
+ {/* Previous Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Next Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Image Container */}
+ e.stopPropagation()}
+ >
+
+
{
+ console.error(`Failed to load lightbox image: ${lightboxImages[lightboxIndex]}`);
+ }}
+ />
+
+ {/* Image Counter */}
+ {lightboxImages.length > 1 && (
+
+ {lightboxIndex + 1} / {lightboxImages.length}
+
+ )}
+
+ {/* Thumbnail Strip (if more than 1 image) */}
+ {lightboxImages.length > 1 && lightboxImages.length <= 10 && (
+
+ {lightboxImages.map((thumb, idx) => (
+
+ ))}
+
+ )}
-
-
+ )}
>
);
};
diff --git a/Frontend/src/pages/admin/BookingManagementPage.tsx b/Frontend/src/pages/admin/BookingManagementPage.tsx
index 9c7bd13f..ce037b6f 100644
--- a/Frontend/src/pages/admin/BookingManagementPage.tsx
+++ b/Frontend/src/pages/admin/BookingManagementPage.tsx
@@ -1,20 +1,23 @@
import React, { useEffect, useState } from 'react';
-import { Search, Eye, XCircle, CheckCircle, Loader2 } from 'lucide-react';
-import { bookingService, Booking } from '../../services/api';
+import { Search, Eye, XCircle, CheckCircle, Loader2, FileText } from 'lucide-react';
+import { bookingService, Booking, invoiceService } from '../../services/api';
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import Pagination from '../../components/common/Pagination';
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { parseDateLocal } from '../../utils/format';
+import { useNavigate } from 'react-router-dom';
const BookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
+ const navigate = useNavigate();
const [bookings, setBookings] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedBooking, setSelectedBooking] = useState(null);
const [showDetailModal, setShowDetailModal] = useState(false);
const [updatingBookingId, setUpdatingBookingId] = useState(null);
const [cancellingBookingId, setCancellingBookingId] = useState(null);
+ const [creatingInvoice, setCreatingInvoice] = useState(false);
const [filters, setFilters] = useState({
search: '',
status: '',
@@ -80,6 +83,32 @@ const BookingManagementPage: React.FC = () => {
}
};
+ const handleCreateInvoice = async (bookingId: number) => {
+ try {
+ setCreatingInvoice(true);
+ // Ensure bookingId is a number
+ const invoiceData = {
+ booking_id: Number(bookingId),
+ };
+
+ const response = await invoiceService.createInvoice(invoiceData);
+
+ if (response.status === 'success' && response.data?.invoice) {
+ toast.success('Invoice created successfully!');
+ setShowDetailModal(false);
+ navigate(`/admin/invoices/${response.data.invoice.id}`);
+ } else {
+ throw new Error('Failed to create invoice');
+ }
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
+ toast.error(errorMessage);
+ console.error('Invoice creation error:', error);
+ } finally {
+ setCreatingInvoice(false);
+ }
+ };
+
const getStatusBadge = (status: string) => {
const badges: Record = {
pending: {
@@ -622,7 +651,24 @@ const BookingManagementPage: React.FC = () => {
{/* Modal Footer */}
-
+
+
- {/* Search Booking */}
+ {/* Date and Search Filters */}
- 1. Search booking
-
-
-
+
+
+
setBookingNumber(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
- placeholder="Enter booking number"
- className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ type="date"
+ value={selectedDate}
+ onChange={(e) => setSelectedDate(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
-
+
+
+
+ setSearchQuery(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSearchByNumber()}
+ placeholder="Enter booking number"
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+
+
- {/* Booking Info */}
+ {/* Check-ins and Check-outs Lists */}
+ {!booking && (
+
+ {/* Check-ins for Today */}
+
+
+
+
+ Check-ins for {formatDate(selectedDate)}
+
+
+ {checkInBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkInBookings.length === 0 ? (
+
+
+ No check-ins scheduled for this date
+
+ ) : (
+
+ {checkInBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Check-outs for Today */}
+
+
+
+
+ Check-outs for {formatDate(selectedDate)}
+
+
+ {checkOutBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkOutBookings.length === 0 ? (
+
+
+ No check-outs scheduled for this date
+
+ ) : (
+
+ {checkOutBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-orange-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Booking Info and Check-in Form */}
{booking && (
<>
-
-
- 2. Booking Information
-
+
+
+
+ 2. Booking Information
+
+
+
@@ -299,7 +541,6 @@ const CheckInPage: React.FC = () => {
{(() => {
- // Use payment_balance from API if available, otherwise calculate from payments
const paymentBalance = booking.payment_balance || (() => {
const completedPayments = booking.payments?.filter(
(p) => p.payment_status === 'completed'
@@ -545,19 +786,6 @@ const CheckInPage: React.FC = () => {
>
)}
-
- {/* Empty State */}
- {!booking && !searching && (
-
-
-
- No booking selected
-
-
- Please enter booking number above to start check-in process
-
-
- )}
);
};
diff --git a/Frontend/src/pages/admin/InvoiceManagementPage.tsx b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
index 3d897a7a..e7d6245d 100644
--- a/Frontend/src/pages/admin/InvoiceManagementPage.tsx
+++ b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
@@ -47,7 +47,8 @@ const InvoiceManagementPage: React.FC = () => {
invoiceList = invoiceList.filter((inv) =>
inv.invoice_number.toLowerCase().includes(filters.search.toLowerCase()) ||
inv.customer_name.toLowerCase().includes(filters.search.toLowerCase()) ||
- inv.customer_email.toLowerCase().includes(filters.search.toLowerCase())
+ inv.customer_email.toLowerCase().includes(filters.search.toLowerCase()) ||
+ (inv.promotion_code && inv.promotion_code.toLowerCase().includes(filters.search.toLowerCase()))
);
}
@@ -130,11 +131,11 @@ const InvoiceManagementPage: React.FC = () => {
Manage and track all invoices
@@ -190,6 +191,9 @@ const InvoiceManagementPage: React.FC = () => {
Amount
+
+ Promotion
+
Status
@@ -235,6 +239,25 @@ const InvoiceManagementPage: React.FC = () => {
Due: {formatCurrency(invoice.balance_due)}
)}
+ {invoice.discount_amount > 0 && (
+
+ Discount: -{formatCurrency(invoice.discount_amount)}
+
+ )}
+
+
+ {invoice.promotion_code ? (
+
+ {invoice.promotion_code}
+
+ ) : (
+ —
+ )}
+ {invoice.is_proforma && (
+
+ Proforma
+
+ )}
@@ -274,7 +297,7 @@ const InvoiceManagementPage: React.FC = () => {
})
) : (
-
+
No invoices found
diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx
index d52089e8..d33edbec 100644
--- a/Frontend/src/pages/admin/PageContentDashboard.tsx
+++ b/Frontend/src/pages/admin/PageContentDashboard.tsx
@@ -7,13 +7,6 @@ import {
Search,
Save,
Globe,
- Facebook,
- Twitter,
- Instagram,
- Linkedin,
- Youtube,
- MapPin,
- Phone,
X,
Plus,
Trash2,
@@ -31,6 +24,8 @@ import { pageContentService, PageContent, PageType, UpdatePageContentData, banne
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import { ConfirmationDialog } from '../../components/common';
+import IconPicker from '../../components/admin/IconPicker';
+import { luxuryContentSeed } from '../../data/luxuryContentSeed';
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo';
@@ -62,7 +57,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -113,6 +108,21 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Helper function to normalize arrays (handle both arrays and JSON strings)
+ const normalizeArray = (value: any): any[] => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ if (typeof value === 'string') {
+ try {
+ const parsed = JSON.parse(value);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+ }
+ return [];
+ };
+
const initializeFormData = (contents: Record) => {
// Home
if (contents.home) {
@@ -130,6 +140,45 @@ const PageContentDashboard: React.FC = () => {
og_title: contents.home.og_title || '',
og_description: contents.home.og_description || '',
og_image: contents.home.og_image || '',
+ features: normalizeArray(contents.home.features),
+ amenities_section_title: contents.home.amenities_section_title || '',
+ amenities_section_subtitle: contents.home.amenities_section_subtitle || '',
+ amenities: normalizeArray(contents.home.amenities),
+ testimonials_section_title: contents.home.testimonials_section_title || '',
+ testimonials_section_subtitle: contents.home.testimonials_section_subtitle || '',
+ testimonials: normalizeArray(contents.home.testimonials),
+ about_preview_title: contents.home.about_preview_title || '',
+ about_preview_subtitle: contents.home.about_preview_subtitle || '',
+ about_preview_content: contents.home.about_preview_content || '',
+ about_preview_image: contents.home.about_preview_image || '',
+ stats: normalizeArray(contents.home.stats),
+ luxury_section_title: contents.home.luxury_section_title || '',
+ luxury_section_subtitle: contents.home.luxury_section_subtitle || '',
+ luxury_section_image: contents.home.luxury_section_image || '',
+ luxury_features: normalizeArray(contents.home.luxury_features),
+ luxury_gallery_section_title: contents.home.luxury_gallery_section_title || '',
+ luxury_gallery_section_subtitle: contents.home.luxury_gallery_section_subtitle || '',
+ luxury_gallery: normalizeArray(contents.home.luxury_gallery),
+ luxury_testimonials_section_title: contents.home.luxury_testimonials_section_title || '',
+ luxury_testimonials_section_subtitle: contents.home.luxury_testimonials_section_subtitle || '',
+ luxury_testimonials: normalizeArray(contents.home.luxury_testimonials),
+ luxury_services_section_title: contents.home.luxury_services_section_title || '',
+ luxury_services_section_subtitle: contents.home.luxury_services_section_subtitle || '',
+ luxury_services: normalizeArray(contents.home.luxury_services),
+ luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
+ luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
+ luxury_experiences: normalizeArray(contents.home.luxury_experiences),
+ awards_section_title: contents.home.awards_section_title || '',
+ awards_section_subtitle: contents.home.awards_section_subtitle || '',
+ awards: normalizeArray(contents.home.awards),
+ cta_title: contents.home.cta_title || '',
+ cta_subtitle: contents.home.cta_subtitle || '',
+ cta_button_text: contents.home.cta_button_text || '',
+ cta_button_link: contents.home.cta_button_link || '',
+ cta_image: contents.home.cta_image || '',
+ partners_section_title: contents.home.partners_section_title || '',
+ partners_section_subtitle: contents.home.partners_section_subtitle || '',
+ partners: normalizeArray(contents.home.partners),
});
}
@@ -154,8 +203,14 @@ const PageContentDashboard: React.FC = () => {
description: contents.about.description || '',
content: contents.about.content || '',
story_content: contents.about.story_content || '',
- values: contents.about.values || [],
- features: contents.about.features || [],
+ values: normalizeArray(contents.about.values),
+ features: normalizeArray(contents.about.features),
+ about_hero_image: contents.about.about_hero_image || '',
+ mission: contents.about.mission || '',
+ vision: contents.about.vision || '',
+ team: normalizeArray(contents.about.team),
+ timeline: normalizeArray(contents.about.timeline),
+ achievements: normalizeArray(contents.about.achievements),
meta_title: contents.about.meta_title || '',
meta_description: contents.about.meta_description || '',
});
@@ -169,6 +224,7 @@ const PageContentDashboard: React.FC = () => {
social_links: contents.footer.social_links || {},
footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] },
badges: contents.footer.badges || [],
+ copyright_text: contents.footer.copyright_text || '',
meta_title: contents.footer.meta_title || '',
meta_description: contents.footer.meta_description || '',
});
@@ -244,7 +300,7 @@ const PageContentDashboard: React.FC = () => {
try {
setUploadingImage(true);
const response = await bannerService.uploadBannerImage(file);
- if (response.status === 'success' || response.success) {
+ if (response.success) {
setBannerFormData({ ...bannerFormData, image_url: response.data.image_url });
toast.success('Image uploaded successfully');
}
@@ -257,6 +313,27 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Generic image upload handler for page content images
+ const handlePageContentImageUpload = async (
+ file: File,
+ onSuccess: (imageUrl: string) => void
+ ) => {
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error('Image size must be less than 5MB');
+ return;
+ }
+
+ try {
+ const response = await pageContentService.uploadImage(file);
+ if (response.success) {
+ onSuccess(response.data.image_url);
+ toast.success('Image uploaded successfully');
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to upload image');
+ }
+ };
+
const handleBannerSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -270,7 +347,7 @@ const PageContentDashboard: React.FC = () => {
if (imageFile && !imageUrl) {
setUploadingImage(true);
const uploadResponse = await bannerService.uploadBannerImage(imageFile);
- if (uploadResponse.status === 'success' || uploadResponse.success) {
+ if (uploadResponse.success) {
imageUrl = uploadResponse.data.image_url;
} else {
throw new Error('Failed to upload image');
@@ -305,9 +382,9 @@ const PageContentDashboard: React.FC = () => {
setEditingBanner(banner);
setBannerFormData({
title: banner.title || '',
- description: '',
+ description: banner.description || '',
image_url: banner.image_url || '',
- link: banner.link || '',
+ link_url: banner.link_url || '',
position: banner.position || 'home',
display_order: banner.display_order || 0,
is_active: banner.is_active ?? true,
@@ -343,7 +420,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -531,6 +608,42 @@ const PageContentDashboard: React.FC = () => {
{/* Home Tab */}
{activeTab === 'home' && (
+ {/* Seed Data Button */}
+
+
+
+ Quick Start
+ Load pre-configured luxury content to get started quickly
+
+
+
+
+
{/* Home Page Content Section */}
Home Page Content
@@ -559,14 +672,38 @@ const PageContentDashboard: React.FC = () => {
-
- setHomeData({ ...homeData, hero_image: e.target.value })}
- className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
- placeholder="https://example.com/hero-image.jpg"
- />
+
+
+ setHomeData({ ...homeData, hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {homeData.hero_image && (
+
+
+
+ )}
@@ -621,17 +758,1388 @@ const PageContentDashboard: React.FC = () => {
placeholder="SEO Meta Description"
/>
+
+
-
-
+ {/* Amenities Section */}
+
+ Amenities Section
+
+
+
+ setHomeData({ ...homeData, amenities_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Amenities"
+ />
+
+
+ setHomeData({ ...homeData, amenities_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience world-class amenities"
+ />
+
+
+
+ Amenities
+
+
+
+ {Array.isArray(homeData.amenities) && homeData.amenities.map((amenity, index) => (
+
+
+ Amenity {index + 1}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], icon: iconName };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], image: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {amenity?.image && (
+
+
+
+ )}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], title: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.amenities || homeData.amenities.length === 0) && (
+ No amenities added yet. Click "Add Amenity" to get started.
+ )}
+
+
+
+ {/* Luxury Section */}
+
+ Luxury Section
+
+
+
+ setHomeData({ ...homeData, luxury_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience Unparalleled Luxury"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Where elegance meets comfort in every detail"
+ />
+
+
+
+
+ setHomeData({ ...homeData, luxury_section_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/luxury-image.jpg or upload"
+ />
+
+
+ {homeData.luxury_section_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Luxury Features Section */}
+
+
+ Luxury Features
+
+
+
+ {Array.isArray(homeData.luxury_features) && homeData.luxury_features.map((feature, index) => (
+
+
+ Luxury Feature {index + 1}
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], icon: iconName };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], title: e.target.value };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Feature Title"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_features || homeData.luxury_features.length === 0) && (
+ No luxury features added yet. Click "Add Luxury Feature" to get started.
+ )}
+
+
+
+ {/* Luxury Gallery Section */}
+
+
+ Luxury Gallery
+
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Gallery"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Discover our exquisite spaces and amenities"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_gallery) && homeData.luxury_gallery.map((image, index) => (
+
+ {
+ setHomeData((prevData) => {
+ const currentGallery = Array.isArray(prevData.luxury_gallery) ? [...prevData.luxury_gallery] : [];
+ currentGallery[index] = e.target.value;
+ return { ...prevData, luxury_gallery: currentGallery };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {image && (
+
+
+
+ )}
+
+ ))}
+ {(!homeData.luxury_gallery || homeData.luxury_gallery.length === 0) && (
+ No gallery images added yet. Click "Add Gallery Image" to get started.
+ )}
+
+
+
+ {/* Luxury Testimonials Section */}
+
+
+ Luxury Testimonials
+
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Guest Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Hear from our valued guests about their luxury stay"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_testimonials) && homeData.luxury_testimonials.map((testimonial, index) => (
+
+
+ Luxury Testimonial {index + 1}
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], name: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], title: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], image: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {testimonial?.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_testimonials || homeData.luxury_testimonials.length === 0) && (
+ No luxury testimonials added yet. Click "Add Luxury Testimonial" to get started.
+ )}
+
+
+
+ {/* About Preview Section */}
+
+ About Preview Section
+
+
+
+ setHomeData({ ...homeData, about_preview_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="About Our Hotel"
+ />
+
+
+
+ setHomeData({ ...homeData, about_preview_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ />
+
+
+
+
+
+
+
+ setHomeData({ ...homeData, about_preview_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/about-image.jpg or upload"
+ />
+
+
+ {homeData.about_preview_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Stats Section */}
+
+
+ Statistics Section
+
+
+
+ {Array.isArray(homeData.stats) && homeData.stats.map((stat, index) => (
+
+
+ Stat {index + 1}
+
+
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], number: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="1000+"
+ />
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], label: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Happy Guests"
+ />
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], icon: iconName };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ label="Icon"
+ />
+
+
+
+ ))}
+ {(!homeData.stats || homeData.stats.length === 0) && (
+ No statistics added yet. Click "Add Stat" to get started.
+ )}
+
+
+
+ {/* Luxury Services Section */}
+
+
+ Luxury Services
+
+
+
+ {Array.isArray(homeData.luxury_services) && homeData.luxury_services.map((service, index) => (
+
+
+ Service {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {service?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Service Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_services || homeData.luxury_services.length === 0) && (
+ No services added yet. Click "Add Service" to get started.
+ )}
+
+
+
+ {/* Luxury Experiences Section */}
+
+
+ Luxury Experiences
+
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unique Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unforgettable moments await"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_experiences) && homeData.luxury_experiences.map((experience, index) => (
+
+
+ Experience {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {experience?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Experience Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_experiences || homeData.luxury_experiences.length === 0) && (
+ No experiences added yet. Click "Add Experience" to get started.
+ )}
+
+
+
+ {/* Awards Section */}
+
+
+ Awards & Certifications
+
+
+
+
+ setHomeData({ ...homeData, awards_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Awards & Recognition"
+ />
+
+
+
+ setHomeData({ ...homeData, awards_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Recognized excellence in hospitality"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.awards) && homeData.awards.map((award, index) => (
+
+
+ Award {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], year: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="2024"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {award?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Award Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.awards || homeData.awards.length === 0) && (
+ No awards added yet. Click "Add Award" to get started.
+ )}
+
+
+
+ {/* CTA Section */}
+
+ Call to Action Section
+
+
+
+ setHomeData({ ...homeData, cta_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Ready to Experience Luxury?"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book your stay today"
+ />
+
+
+
+
+ setHomeData({ ...homeData, cta_button_text: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book Now"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_button_link: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="/rooms"
+ />
+
+
+
+
+
+ setHomeData({ ...homeData, cta_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/cta-image.jpg or upload"
+ />
+
+
+ {homeData.cta_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Partners Section */}
+
+
+ Partners & Brands
+
+
+
+
+ setHomeData({ ...homeData, partners_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Our Partners"
+ />
+
+
+
+ setHomeData({ ...homeData, partners_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Trusted by leading brands"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.partners) && homeData.partners.map((partner, index) => (
+
+
+ Partner {index + 1}
+
+
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], name: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Partner Name"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], logo: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com/logo.png"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], link: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com"
+ />
+
+
+
+ ))}
+ {(!homeData.partners || homeData.partners.length === 0) && (
+ No partners added yet. Click "Add Partner" to get started.
+ )}
+
+
+
+ {/* Save Button */}
+
+
+
@@ -696,8 +2204,11 @@ const PageContentDashboard: React.FC = () => {
{banner.title}
- {banner.link && (
- {banner.link}
+ {banner.description && (
+ {banner.description}
+ )}
+ {banner.link_url && (
+ {banner.link_url}
)}
@@ -781,12 +2292,24 @@ const PageContentDashboard: React.FC = () => {
/>
+
+
+
+
setBannerFormData({ ...bannerFormData, link: e.target.value })}
+ value={bannerFormData.link_url}
+ onChange={(e) => setBannerFormData({ ...bannerFormData, link_url: e.target.value })}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
placeholder="https://example.com"
/>
@@ -1140,6 +2663,690 @@ const PageContentDashboard: React.FC = () => {
/>
+ {/* Hero Image */}
+
+
+
+ setAboutData({ ...aboutData, about_hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {aboutData.about_hero_image && (
+
+
+
+ )}
+
+
+ {/* Mission */}
+
+
+
+
+ {/* Vision */}
+
+
+
+
+ {/* Values Section */}
+
+
+ Our Values
+
+
+ {Array.isArray(aboutData.values) && aboutData.values.length > 0 ? (
+
+ {aboutData.values.map((value, index) => (
+
+
+ Value {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], icon };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], title: e.target.value };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Value title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No values added yet. Click "Add Value" to get started.
+ )}
+
+
+ {/* Features Section */}
+
+
+ Features
+
+
+ {Array.isArray(aboutData.features) && aboutData.features.length > 0 ? (
+
+ {aboutData.features.map((feature, index) => (
+
+
+ Feature {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], icon };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], title: e.target.value };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Feature title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No features added yet. Click "Add Feature" to get started.
+ )}
+
+
+ {/* Team Section */}
+
+
+ Team Members
+
+
+ {Array.isArray(aboutData.team) && aboutData.team.length > 0 ? (
+
+ {aboutData.team.map((member, index) => (
+
+
+ Team Member {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], name: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Full name"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], role: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Job title"
+ />
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], image: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {member.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+
+ ) : (
+ No team members added yet. Click "Add Team Member" to get started.
+ )}
+
+
+ {/* Timeline Section */}
+
+
+ Timeline / History
+
+
+ {Array.isArray(aboutData.timeline) && aboutData.timeline.length > 0 ? (
+
+ {aboutData.timeline.map((event, index) => (
+
+
+ Event {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], year: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], title: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Event title"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], image: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {event.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No timeline events added yet. Click "Add Event" to get started.
+ )}
+
+
+ {/* Achievements Section */}
+
+
+ Achievements & Awards
+
+
+ {Array.isArray(aboutData.achievements) && aboutData.achievements.length > 0 ? (
+
+ {aboutData.achievements.map((achievement, index) => (
+
+
+ Achievement {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], icon };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ />
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], title: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Achievement title"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], year: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], image: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {achievement.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No achievements added yet. Click "Add Achievement" to get started.
+ )}
+
+
@@ -1400,6 +3607,28 @@ const PageContentDashboard: React.FC = () => {
+ {/* Copyright Text */}
+
+ Copyright Text
+
+
+
+ setFooterData({ ...footerData, copyright_text: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200"
+ placeholder="© {YEAR} Luxury Hotel. All rights reserved."
+ />
+
+ The {`{YEAR}`} placeholder will be automatically replaced with the current year.
+
+
+
+
+
Why Choose Us
- ++ {feature.title} +
++ {feature.description} +
+- {feature.title} -
-- {feature.description} -
-Our Mission
+{pageContent.mission}
+Our Vision
+{pageContent.vision}
++ Our Team +
++ Our History +
+{event.title}
+{event.description}
+ {event.image && ( ++ Achievements & Awards +
+{achievement.title}
+{achievement.description}
+ {achievement.image && ( +
+
+
+
+
+
+
+ Connect With Us
+
+
Get In Touch
-
-
+
+
+
+
+
+
We'd love to hear from you. Contact us for reservations or inquiries.
-
-
-
-
+
+
+
+
-
+
Address
-
+
{displayAddress
.split('\n').map((line, i) => (
@@ -269,40 +587,41 @@ const AboutPage: React.FC = () => {
))}
-
-
-
+
-
-
-
+
-
+
- Explore Our Rooms
-
+ Explore Our Rooms
+
+
diff --git a/Frontend/src/pages/ContactPage.tsx b/Frontend/src/pages/ContactPage.tsx
index 32668000..619729da 100644
--- a/Frontend/src/pages/ContactPage.tsx
+++ b/Frontend/src/pages/ContactPage.tsx
@@ -99,7 +99,7 @@ const ContactPage: React.FC = () => {
useEffect(() => {
const fetchPageContent = async () => {
try {
- const response = await pageContentService.getPageContent('contact');
+ const response = await pageContentService.getContactContent();
if (response.status === 'success' && response.data?.page_content) {
setPageContent(response.data.page_content);
diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx
index 723abf87..15ff2648 100644
--- a/Frontend/src/pages/HomePage.tsx
+++ b/Frontend/src/pages/HomePage.tsx
@@ -3,7 +3,13 @@ import { Link } from 'react-router-dom';
import {
ArrowRight,
AlertCircle,
+ Star,
+ X,
+ ChevronLeft,
+ ChevronRight,
+ ZoomIn,
} from 'lucide-react';
+import * as LucideIcons from 'lucide-react';
import {
BannerCarousel,
BannerSkeleton,
@@ -32,6 +38,33 @@ const HomePage: React.FC = () => {
const [isLoadingNewest, setIsLoadingNewest] = useState(true);
const [isLoadingContent, setIsLoadingContent] = useState(true);
const [error, setError] = useState(null);
+ const [lightboxOpen, setLightboxOpen] = useState(false);
+ const [lightboxIndex, setLightboxIndex] = useState(0);
+ const [lightboxImages, setLightboxImages] = useState([]);
+
+ // Handle keyboard navigation for lightbox
+ useEffect(() => {
+ if (!lightboxOpen) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setLightboxOpen(false);
+ } else if (e.key === 'ArrowLeft' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === 0 ? lightboxImages.length - 1 : prev - 1));
+ } else if (e.key === 'ArrowRight' && lightboxImages.length > 1) {
+ setLightboxIndex((prev) => (prev === lightboxImages.length - 1 ? 0 : prev + 1));
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ // Prevent body scroll when lightbox is open
+ document.body.style.overflow = 'hidden';
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ document.body.style.overflow = 'unset';
+ };
+ }, [lightboxOpen, lightboxImages.length]);
// Combine featured and newest rooms, removing duplicates
const combinedRooms = useMemo(() => {
@@ -57,22 +90,118 @@ const HomePage: React.FC = () => {
const fetchPageContent = async () => {
try {
setIsLoadingContent(true);
- const response = await pageContentService.getPageContent('home');
+ const response = await pageContentService.getHomeContent();
if (response.status === 'success' && response.data?.page_content) {
- setPageContent(response.data.page_content);
+ const content = response.data.page_content;
+
+ // Parse JSON fields if they come as strings (backward compatibility)
+ if (typeof content.features === 'string') {
+ try {
+ content.features = JSON.parse(content.features);
+ } catch (e) {
+ content.features = [];
+ }
+ }
+ if (typeof content.amenities === 'string') {
+ try {
+ content.amenities = JSON.parse(content.amenities);
+ } catch (e) {
+ content.amenities = [];
+ }
+ }
+ if (typeof content.testimonials === 'string') {
+ try {
+ content.testimonials = JSON.parse(content.testimonials);
+ } catch (e) {
+ content.testimonials = [];
+ }
+ }
+ if (typeof content.gallery_images === 'string') {
+ try {
+ content.gallery_images = JSON.parse(content.gallery_images);
+ } catch (e) {
+ content.gallery_images = [];
+ }
+ }
+ if (typeof content.stats === 'string') {
+ try {
+ content.stats = JSON.parse(content.stats);
+ } catch (e) {
+ content.stats = [];
+ }
+ }
+ // Parse luxury fields
+ if (typeof content.luxury_features === 'string') {
+ try {
+ content.luxury_features = JSON.parse(content.luxury_features);
+ } catch (e) {
+ content.luxury_features = [];
+ }
+ }
+ if (typeof content.luxury_gallery === 'string') {
+ try {
+ const parsed = JSON.parse(content.luxury_gallery);
+ content.luxury_gallery = Array.isArray(parsed) ? parsed.filter(img => img && typeof img === 'string' && img.trim() !== '') : [];
+ } catch (e) {
+ content.luxury_gallery = [];
+ }
+ }
+ // Ensure luxury_gallery is an array and filter out empty values
+ if (Array.isArray(content.luxury_gallery)) {
+ content.luxury_gallery = content.luxury_gallery.filter(img => img && typeof img === 'string' && img.trim() !== '');
+ } else {
+ content.luxury_gallery = [];
+ }
+ if (typeof content.luxury_testimonials === 'string') {
+ try {
+ content.luxury_testimonials = JSON.parse(content.luxury_testimonials);
+ } catch (e) {
+ content.luxury_testimonials = [];
+ }
+ }
+ if (typeof content.luxury_services === 'string') {
+ try {
+ content.luxury_services = JSON.parse(content.luxury_services);
+ } catch (e) {
+ content.luxury_services = [];
+ }
+ }
+ if (typeof content.luxury_experiences === 'string') {
+ try {
+ content.luxury_experiences = JSON.parse(content.luxury_experiences);
+ } catch (e) {
+ content.luxury_experiences = [];
+ }
+ }
+ if (typeof content.awards === 'string') {
+ try {
+ content.awards = JSON.parse(content.awards);
+ } catch (e) {
+ content.awards = [];
+ }
+ }
+ if (typeof content.partners === 'string') {
+ try {
+ content.partners = JSON.parse(content.partners);
+ } catch (e) {
+ content.partners = [];
+ }
+ }
+
+ setPageContent(content);
// Update document title and meta tags
- if (response.data.page_content.meta_title) {
- document.title = response.data.page_content.meta_title;
+ if (content.meta_title) {
+ document.title = content.meta_title;
}
- if (response.data.page_content.meta_description) {
+ if (content.meta_description) {
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
- metaDescription.setAttribute('content', response.data.page_content.meta_description);
+ metaDescription.setAttribute('content', content.meta_description);
}
}
} catch (err: any) {
@@ -215,27 +344,34 @@ const HomePage: React.FC = () => {
)}
-
+
+ {/* Subtle background pattern */}
+
+
{/* Featured & Newest Rooms Section - Combined Carousel */}
-
+
{/* Section Header - Centered */}
-
+
+
+
+
{pageContent?.hero_title || 'Featured & Newest Rooms'}
-
+
{pageContent?.hero_subtitle || pageContent?.description || 'Discover our most popular accommodations and latest additions'}
{/* View All Rooms Button - Golden, Centered */}
-
+
+
View All Rooms
-
+
@@ -300,79 +436,818 @@ const HomePage: React.FC = () => {
)}
- {/* Features Section */}
-
-
- {/* Decorative gold accent */}
-
-
-
-
-
- 🏨
-
-
- Easy Booking
-
-
- Search and book rooms with just a few clicks
-
-
+ {/* Features Section - Dynamic from page content */}
+ {(() => {
+ // Filter out empty features (no title or description)
+ const validFeatures = pageContent?.features?.filter(
+ (f: any) => f && (f.title || f.description)
+ ) || [];
+
+ return (validFeatures.length > 0 || !pageContent) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+ {/* Subtle background pattern */}
+
+
+
+ {validFeatures.length > 0 ? (
+ validFeatures.map((feature: any, index: number) => (
+
+ {feature.image ? (
+
+
+
+ ) : (
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+ {feature.title && (
+
+ {feature.title}
+
+ )}
+ {feature.description && (
+
+ {feature.description}
+
+ )}
+
+ ))
+ ) : (
+ <>
+
+
+ 🏨
+
+
+ Easy Booking
+
+
+ Search and book rooms with just a few clicks
+
+
-
-
- 💰
-
-
- Best Prices
-
-
- Best price guarantee in the market
-
-
+
+
+ 💰
+
+
+ Best Prices
+
+
+ Best price guarantee in the market
+
+
-
-
- 🎧
+
+
+ 🎧
+
+
+ 24/7 Support
+
+
+ Support team always ready to serve
+
+
+ >
+ )}
-
- 24/7 Support
-
-
- Support team always ready to serve
+
+
+ );
+ })()}
+
+ {/* Luxury Section - Dynamic from page content */}
+ {(pageContent?.luxury_section_title || (pageContent?.luxury_features && pageContent.luxury_features.length > 0)) && (
+
+
+
+
+
+
+ {pageContent.luxury_section_title || 'Experience Unparalleled Luxury'}
+
+ {pageContent.luxury_section_subtitle && (
+
+ {pageContent.luxury_section_subtitle}
+ )}
+
+ {pageContent.luxury_section_image && (
+
+
+
+
+
+
+ )}
+ {pageContent.luxury_features && pageContent.luxury_features.length > 0 && (
+
+ {pageContent.luxury_features.map((feature, index) => (
+
+
+
+ {feature.icon && (LucideIcons as any)[feature.icon] ? (
+ React.createElement((LucideIcons as any)[feature.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Luxury Gallery Section - Dynamic from page content */}
+ {pageContent?.luxury_gallery && Array.isArray(pageContent.luxury_gallery) && pageContent.luxury_gallery.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_gallery_section_title || 'Luxury Gallery'}
+
+ {pageContent.luxury_gallery_section_subtitle && (
+
+ {pageContent.luxury_gallery_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_gallery.map((image, index) => {
+ // Normalize image URL - if it's a relative path, prepend the API URL
+ const imageUrl = image && typeof image === 'string'
+ ? (image.startsWith('http://') || image.startsWith('https://')
+ ? image
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${image.startsWith('/') ? image : '/' + image}`)
+ : '';
+
+ if (!imageUrl) return null;
+
+ return (
+ {
+ const normalizedImages = pageContent.luxury_gallery
+ .map(img => {
+ if (!img || typeof img !== 'string') return null;
+ return img.startsWith('http://') || img.startsWith('https://')
+ ? img
+ : `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8000'}${img.startsWith('/') ? img : '/' + img}`;
+ })
+ .filter(Boolean) as string[];
+ setLightboxImages(normalizedImages);
+ setLightboxIndex(index);
+ setLightboxOpen(true);
+ }}
+ >
+
{
+ console.error(`Failed to load luxury gallery image: ${imageUrl}`);
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Luxury Testimonials Section - Dynamic from page content */}
+ {pageContent?.luxury_testimonials && pageContent.luxury_testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_testimonials_section_title || 'Guest Experiences'}
+
+ {pageContent.luxury_testimonials_section_subtitle && (
+
+ {pageContent.luxury_testimonials_section_subtitle}
+
+ )}
+
+ Hear from our valued guests about their luxury stay
+
+
+
+ {pageContent.luxury_testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.title && (
+ {testimonial.title}
+ )}
+
+
+
+ "
+ {testimonial.quote}
+
+
+ ))}
+
+
+ )}
+
+ {/* Stats Section - Dynamic from page content */}
+ {pageContent?.stats && Array.isArray(pageContent.stats) && pageContent.stats.length > 0 && (
+
+
+ {/* Decorative elements */}
+
+
+
+
+ {pageContent.stats.map((stat, index) => (
+
+ {stat?.icon && (
+
+ {stat.icon && (LucideIcons as any)[stat.icon] ? (
+ React.createElement((LucideIcons as any)[stat.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ {stat.icon}
+ )}
+
+ )}
+ {stat?.number && (
+
+ {stat.number}
+
+ )}
+ {stat?.label && (
+
+ {stat.label}
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {/* Amenities Section - Dynamic from page content */}
+ {pageContent?.amenities && pageContent.amenities.length > 0 && (
+
+
+
+
+
+
+ {pageContent.amenities_section_title || 'Luxury Amenities'}
+
+
+ {pageContent.amenities_section_subtitle || 'Experience world-class amenities designed for your comfort'}
+
+
+
+ {pageContent.amenities.map((amenity, index) => (
+
+
+ {amenity.image ? (
+
+
+
+ ) : (
+
+ {amenity.icon && (LucideIcons as any)[amenity.icon] ? (
+ React.createElement((LucideIcons as any)[amenity.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {amenity.title}
+
+
+ {amenity.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Testimonials Section - Dynamic from page content */}
+ {pageContent?.testimonials && pageContent.testimonials.length > 0 && (
+
+
+
+
+
+
+ {pageContent.testimonials_section_title || 'Guest Testimonials'}
+
+
+ {pageContent.testimonials_section_subtitle || 'See what our guests say about their experience'}
+
+
+
+ {pageContent.testimonials.map((testimonial, index) => (
+
+
+
+ {testimonial.image ? (
+
+
+
+
+ ) : (
+
+ {testimonial.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {testimonial.name}
+ {testimonial.role}
+
+
+
+ {[...Array(5)].map((_, i) => (
+ ★
+ ))}
+
+
+ "
+ "{testimonial.comment}"
+
+
+ ))}
+
+
+ )}
+
+
+ {/* About Preview Section - Dynamic from page content */}
+ {(pageContent?.about_preview_title || pageContent?.about_preview_content) && (
+
+
+ {/* Decorative gold accents */}
+
+
+
+
+ {pageContent.about_preview_image && (
+
+
+
+
+ )}
+
+
+
+
+
+ {pageContent.about_preview_title || 'About Our Hotel'}
+
+ {pageContent.about_preview_subtitle && (
+
+ {pageContent.about_preview_subtitle}
+
+ )}
+ {pageContent.about_preview_content && (
+
+ {pageContent.about_preview_content}
+
+ )}
+
+
+ Learn More
+
+
+
+
+
+
+ )}
+
+ {/* Luxury Services Section */}
+ {pageContent?.luxury_services && pageContent.luxury_services.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_services_section_title || 'Luxury Services'}
+
+ {pageContent.luxury_services_section_subtitle && (
+
+ {pageContent.luxury_services_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_services.map((service: any, index: number) => (
+
+
+ {service.image ? (
+
+
+
+ ) : (
+
+ {service.icon && (LucideIcons as any)[service.icon] ? (
+ React.createElement((LucideIcons as any)[service.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {service.title}
+
+
+ {service.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Luxury Experiences Section */}
+ {pageContent?.luxury_experiences && pageContent.luxury_experiences.length > 0 && (
+
+
+
+
+
+
+ {pageContent.luxury_experiences_section_title || 'Unique Experiences'}
+
+ {pageContent.luxury_experiences_section_subtitle && (
+
+ {pageContent.luxury_experiences_section_subtitle}
+
+ )}
+
+
+ {pageContent.luxury_experiences.map((experience: any, index: number) => (
+
+
+ {experience.image ? (
+
+
+
+ ) : (
+
+ {experience.icon && (LucideIcons as any)[experience.icon] ? (
+ React.createElement((LucideIcons as any)[experience.icon], {
+ className: 'w-7 h-7 md:w-8 md:h-8 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ ✨
+ )}
+
+ )}
+
+ {experience.title}
+
+
+ {experience.description}
+
+
+ ))}
+
+
+ )}
+
+ {/* Awards Section */}
+ {pageContent?.awards && pageContent.awards.length > 0 && (
+
+
+
+
+
+
+ {pageContent.awards_section_title || 'Awards & Recognition'}
+
+ {pageContent.awards_section_subtitle && (
+
+ {pageContent.awards_section_subtitle}
+
+ )}
+
+
+ {pageContent.awards.map((award: any, index: number) => (
+
+
+ {award.image ? (
+
+
+
+ ) : (
+
+ {award.icon && (LucideIcons as any)[award.icon] ? (
+ React.createElement((LucideIcons as any)[award.icon], {
+ className: 'w-8 h-8 md:w-10 md:h-10 text-[#d4af37] drop-shadow-md'
+ })
+ ) : (
+ 🏆
+ )}
+
+ )}
+ {award.year && (
+ {award.year}
+ )}
+
+ {award.title}
+
+ {award.description && (
+
+ {award.description}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* CTA Section */}
+ {(pageContent?.cta_title || pageContent?.cta_subtitle) && (
+
+
+ {pageContent.cta_image && (
+
+
+
+ )}
+
+
+
+
+
+
+ {pageContent.cta_title}
+
+ {pageContent.cta_subtitle && (
+
+ {pageContent.cta_subtitle}
+
+ )}
+ {pageContent.cta_button_text && pageContent.cta_button_link && (
+
+
+ {pageContent.cta_button_text}
+
+
+ )}
+
+
+
+ )}
+
+ {/* Partners Section */}
+ {pageContent?.partners && pageContent.partners.length > 0 && (
+
+
+
+
+
+
+ {pageContent.partners_section_title || 'Our Partners'}
+
+ {pageContent.partners_section_subtitle && (
+
+ {pageContent.partners_section_subtitle}
+
+ )}
+
+
+ {pageContent.partners.map((partner: any, index: number) => (
+
+ {partner.link ? (
+
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+
+ ) : (
+ <>
+ {partner.logo ? (
+
+ ) : (
+ {partner.name}
+ )}
+ >
+ )}
+
+ ))}
+
+
+ )}
+
+
+
+
+ {/* Luxury Gallery Lightbox Modal */}
+ {lightboxOpen && lightboxImages.length > 0 && (
+ setLightboxOpen(false)}
+ >
+ {/* Close Button */}
+
+
+ {/* Previous Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Next Button */}
+ {lightboxImages.length > 1 && (
+
+ )}
+
+ {/* Image Container */}
+ e.stopPropagation()}
+ >
+
+
{
+ console.error(`Failed to load lightbox image: ${lightboxImages[lightboxIndex]}`);
+ }}
+ />
+
+ {/* Image Counter */}
+ {lightboxImages.length > 1 && (
+
+ {lightboxIndex + 1} / {lightboxImages.length}
+
+ )}
+
+ {/* Thumbnail Strip (if more than 1 image) */}
+ {lightboxImages.length > 1 && lightboxImages.length <= 10 && (
+
+ {lightboxImages.map((thumb, idx) => (
+
+ ))}
+
+ )}
-
-
+ )}
>
);
};
diff --git a/Frontend/src/pages/admin/BookingManagementPage.tsx b/Frontend/src/pages/admin/BookingManagementPage.tsx
index 9c7bd13f..ce037b6f 100644
--- a/Frontend/src/pages/admin/BookingManagementPage.tsx
+++ b/Frontend/src/pages/admin/BookingManagementPage.tsx
@@ -1,20 +1,23 @@
import React, { useEffect, useState } from 'react';
-import { Search, Eye, XCircle, CheckCircle, Loader2 } from 'lucide-react';
-import { bookingService, Booking } from '../../services/api';
+import { Search, Eye, XCircle, CheckCircle, Loader2, FileText } from 'lucide-react';
+import { bookingService, Booking, invoiceService } from '../../services/api';
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import Pagination from '../../components/common/Pagination';
import { useFormatCurrency } from '../../hooks/useFormatCurrency';
import { parseDateLocal } from '../../utils/format';
+import { useNavigate } from 'react-router-dom';
const BookingManagementPage: React.FC = () => {
const { formatCurrency } = useFormatCurrency();
+ const navigate = useNavigate();
const [bookings, setBookings] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedBooking, setSelectedBooking] = useState(null);
const [showDetailModal, setShowDetailModal] = useState(false);
const [updatingBookingId, setUpdatingBookingId] = useState(null);
const [cancellingBookingId, setCancellingBookingId] = useState(null);
+ const [creatingInvoice, setCreatingInvoice] = useState(false);
const [filters, setFilters] = useState({
search: '',
status: '',
@@ -80,6 +83,32 @@ const BookingManagementPage: React.FC = () => {
}
};
+ const handleCreateInvoice = async (bookingId: number) => {
+ try {
+ setCreatingInvoice(true);
+ // Ensure bookingId is a number
+ const invoiceData = {
+ booking_id: Number(bookingId),
+ };
+
+ const response = await invoiceService.createInvoice(invoiceData);
+
+ if (response.status === 'success' && response.data?.invoice) {
+ toast.success('Invoice created successfully!');
+ setShowDetailModal(false);
+ navigate(`/admin/invoices/${response.data.invoice.id}`);
+ } else {
+ throw new Error('Failed to create invoice');
+ }
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message || 'Unable to create invoice';
+ toast.error(errorMessage);
+ console.error('Invoice creation error:', error);
+ } finally {
+ setCreatingInvoice(false);
+ }
+ };
+
const getStatusBadge = (status: string) => {
const badges: Record = {
pending: {
@@ -622,7 +651,24 @@ const BookingManagementPage: React.FC = () => {
{/* Modal Footer */}
-
+
+
- {/* Search Booking */}
+ {/* Date and Search Filters */}
- 1. Search booking
-
-
-
+
+
+
setBookingNumber(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
- placeholder="Enter booking number"
- className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ type="date"
+ value={selectedDate}
+ onChange={(e) => setSelectedDate(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
-
+
+
+
+ setSearchQuery(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSearchByNumber()}
+ placeholder="Enter booking number"
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+
+
- {/* Booking Info */}
+ {/* Check-ins and Check-outs Lists */}
+ {!booking && (
+
+ {/* Check-ins for Today */}
+
+
+
+
+ Check-ins for {formatDate(selectedDate)}
+
+
+ {checkInBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkInBookings.length === 0 ? (
+
+
+ No check-ins scheduled for this date
+
+ ) : (
+
+ {checkInBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Check-outs for Today */}
+
+
+
+
+ Check-outs for {formatDate(selectedDate)}
+
+
+ {checkOutBookings.length}
+
+
+ {loadingBookings ? (
+
+
+
+ ) : checkOutBookings.length === 0 ? (
+
+
+ No check-outs scheduled for this date
+
+ ) : (
+
+ {checkOutBookings.map((b) => (
+ handleSelectBooking(b.booking_number)}
+ className="p-4 border border-gray-200 rounded-lg hover:border-orange-500 hover:shadow-md cursor-pointer transition-all"
+ >
+
+
+ {b.booking_number}
+ {b.user?.full_name}
+
+ {b.room?.room_type?.name} • {formatCurrency(b.total_price)}
+
+
+
+
+ {b.status}
+
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Booking Info and Check-in Form */}
{booking && (
<>
-
-
- 2. Booking Information
-
+
+
+
+ 2. Booking Information
+
+
+
@@ -299,7 +541,6 @@ const CheckInPage: React.FC = () => {
{(() => {
- // Use payment_balance from API if available, otherwise calculate from payments
const paymentBalance = booking.payment_balance || (() => {
const completedPayments = booking.payments?.filter(
(p) => p.payment_status === 'completed'
@@ -545,19 +786,6 @@ const CheckInPage: React.FC = () => {
>
)}
-
- {/* Empty State */}
- {!booking && !searching && (
-
-
-
- No booking selected
-
-
- Please enter booking number above to start check-in process
-
-
- )}
);
};
diff --git a/Frontend/src/pages/admin/InvoiceManagementPage.tsx b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
index 3d897a7a..e7d6245d 100644
--- a/Frontend/src/pages/admin/InvoiceManagementPage.tsx
+++ b/Frontend/src/pages/admin/InvoiceManagementPage.tsx
@@ -47,7 +47,8 @@ const InvoiceManagementPage: React.FC = () => {
invoiceList = invoiceList.filter((inv) =>
inv.invoice_number.toLowerCase().includes(filters.search.toLowerCase()) ||
inv.customer_name.toLowerCase().includes(filters.search.toLowerCase()) ||
- inv.customer_email.toLowerCase().includes(filters.search.toLowerCase())
+ inv.customer_email.toLowerCase().includes(filters.search.toLowerCase()) ||
+ (inv.promotion_code && inv.promotion_code.toLowerCase().includes(filters.search.toLowerCase()))
);
}
@@ -130,11 +131,11 @@ const InvoiceManagementPage: React.FC = () => {
Manage and track all invoices
@@ -190,6 +191,9 @@ const InvoiceManagementPage: React.FC = () => {
Amount
+
+ Promotion
+
Status
@@ -235,6 +239,25 @@ const InvoiceManagementPage: React.FC = () => {
Due: {formatCurrency(invoice.balance_due)}
)}
+ {invoice.discount_amount > 0 && (
+
+ Discount: -{formatCurrency(invoice.discount_amount)}
+
+ )}
+
+
+ {invoice.promotion_code ? (
+
+ {invoice.promotion_code}
+
+ ) : (
+ —
+ )}
+ {invoice.is_proforma && (
+
+ Proforma
+
+ )}
@@ -274,7 +297,7 @@ const InvoiceManagementPage: React.FC = () => {
})
) : (
-
+
No invoices found
diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx
index d52089e8..d33edbec 100644
--- a/Frontend/src/pages/admin/PageContentDashboard.tsx
+++ b/Frontend/src/pages/admin/PageContentDashboard.tsx
@@ -7,13 +7,6 @@ import {
Search,
Save,
Globe,
- Facebook,
- Twitter,
- Instagram,
- Linkedin,
- Youtube,
- MapPin,
- Phone,
X,
Plus,
Trash2,
@@ -31,6 +24,8 @@ import { pageContentService, PageContent, PageType, UpdatePageContentData, banne
import { toast } from 'react-toastify';
import Loading from '../../components/common/Loading';
import { ConfirmationDialog } from '../../components/common';
+import IconPicker from '../../components/admin/IconPicker';
+import { luxuryContentSeed } from '../../data/luxuryContentSeed';
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo';
@@ -62,7 +57,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -113,6 +108,21 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Helper function to normalize arrays (handle both arrays and JSON strings)
+ const normalizeArray = (value: any): any[] => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ if (typeof value === 'string') {
+ try {
+ const parsed = JSON.parse(value);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+ }
+ return [];
+ };
+
const initializeFormData = (contents: Record) => {
// Home
if (contents.home) {
@@ -130,6 +140,45 @@ const PageContentDashboard: React.FC = () => {
og_title: contents.home.og_title || '',
og_description: contents.home.og_description || '',
og_image: contents.home.og_image || '',
+ features: normalizeArray(contents.home.features),
+ amenities_section_title: contents.home.amenities_section_title || '',
+ amenities_section_subtitle: contents.home.amenities_section_subtitle || '',
+ amenities: normalizeArray(contents.home.amenities),
+ testimonials_section_title: contents.home.testimonials_section_title || '',
+ testimonials_section_subtitle: contents.home.testimonials_section_subtitle || '',
+ testimonials: normalizeArray(contents.home.testimonials),
+ about_preview_title: contents.home.about_preview_title || '',
+ about_preview_subtitle: contents.home.about_preview_subtitle || '',
+ about_preview_content: contents.home.about_preview_content || '',
+ about_preview_image: contents.home.about_preview_image || '',
+ stats: normalizeArray(contents.home.stats),
+ luxury_section_title: contents.home.luxury_section_title || '',
+ luxury_section_subtitle: contents.home.luxury_section_subtitle || '',
+ luxury_section_image: contents.home.luxury_section_image || '',
+ luxury_features: normalizeArray(contents.home.luxury_features),
+ luxury_gallery_section_title: contents.home.luxury_gallery_section_title || '',
+ luxury_gallery_section_subtitle: contents.home.luxury_gallery_section_subtitle || '',
+ luxury_gallery: normalizeArray(contents.home.luxury_gallery),
+ luxury_testimonials_section_title: contents.home.luxury_testimonials_section_title || '',
+ luxury_testimonials_section_subtitle: contents.home.luxury_testimonials_section_subtitle || '',
+ luxury_testimonials: normalizeArray(contents.home.luxury_testimonials),
+ luxury_services_section_title: contents.home.luxury_services_section_title || '',
+ luxury_services_section_subtitle: contents.home.luxury_services_section_subtitle || '',
+ luxury_services: normalizeArray(contents.home.luxury_services),
+ luxury_experiences_section_title: contents.home.luxury_experiences_section_title || '',
+ luxury_experiences_section_subtitle: contents.home.luxury_experiences_section_subtitle || '',
+ luxury_experiences: normalizeArray(contents.home.luxury_experiences),
+ awards_section_title: contents.home.awards_section_title || '',
+ awards_section_subtitle: contents.home.awards_section_subtitle || '',
+ awards: normalizeArray(contents.home.awards),
+ cta_title: contents.home.cta_title || '',
+ cta_subtitle: contents.home.cta_subtitle || '',
+ cta_button_text: contents.home.cta_button_text || '',
+ cta_button_link: contents.home.cta_button_link || '',
+ cta_image: contents.home.cta_image || '',
+ partners_section_title: contents.home.partners_section_title || '',
+ partners_section_subtitle: contents.home.partners_section_subtitle || '',
+ partners: normalizeArray(contents.home.partners),
});
}
@@ -154,8 +203,14 @@ const PageContentDashboard: React.FC = () => {
description: contents.about.description || '',
content: contents.about.content || '',
story_content: contents.about.story_content || '',
- values: contents.about.values || [],
- features: contents.about.features || [],
+ values: normalizeArray(contents.about.values),
+ features: normalizeArray(contents.about.features),
+ about_hero_image: contents.about.about_hero_image || '',
+ mission: contents.about.mission || '',
+ vision: contents.about.vision || '',
+ team: normalizeArray(contents.about.team),
+ timeline: normalizeArray(contents.about.timeline),
+ achievements: normalizeArray(contents.about.achievements),
meta_title: contents.about.meta_title || '',
meta_description: contents.about.meta_description || '',
});
@@ -169,6 +224,7 @@ const PageContentDashboard: React.FC = () => {
social_links: contents.footer.social_links || {},
footer_links: contents.footer.footer_links || { quick_links: [], support_links: [] },
badges: contents.footer.badges || [],
+ copyright_text: contents.footer.copyright_text || '',
meta_title: contents.footer.meta_title || '',
meta_description: contents.footer.meta_description || '',
});
@@ -244,7 +300,7 @@ const PageContentDashboard: React.FC = () => {
try {
setUploadingImage(true);
const response = await bannerService.uploadBannerImage(file);
- if (response.status === 'success' || response.success) {
+ if (response.success) {
setBannerFormData({ ...bannerFormData, image_url: response.data.image_url });
toast.success('Image uploaded successfully');
}
@@ -257,6 +313,27 @@ const PageContentDashboard: React.FC = () => {
}
};
+ // Generic image upload handler for page content images
+ const handlePageContentImageUpload = async (
+ file: File,
+ onSuccess: (imageUrl: string) => void
+ ) => {
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error('Image size must be less than 5MB');
+ return;
+ }
+
+ try {
+ const response = await pageContentService.uploadImage(file);
+ if (response.success) {
+ onSuccess(response.data.image_url);
+ toast.success('Image uploaded successfully');
+ }
+ } catch (error: any) {
+ toast.error(error.response?.data?.message || 'Failed to upload image');
+ }
+ };
+
const handleBannerSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -270,7 +347,7 @@ const PageContentDashboard: React.FC = () => {
if (imageFile && !imageUrl) {
setUploadingImage(true);
const uploadResponse = await bannerService.uploadBannerImage(imageFile);
- if (uploadResponse.status === 'success' || uploadResponse.success) {
+ if (uploadResponse.success) {
imageUrl = uploadResponse.data.image_url;
} else {
throw new Error('Failed to upload image');
@@ -305,9 +382,9 @@ const PageContentDashboard: React.FC = () => {
setEditingBanner(banner);
setBannerFormData({
title: banner.title || '',
- description: '',
+ description: banner.description || '',
image_url: banner.image_url || '',
- link: banner.link || '',
+ link_url: banner.link_url || '',
position: banner.position || 'home',
display_order: banner.display_order || 0,
is_active: banner.is_active ?? true,
@@ -343,7 +420,7 @@ const PageContentDashboard: React.FC = () => {
title: '',
description: '',
image_url: '',
- link: '',
+ link_url: '',
position: 'home',
display_order: 0,
is_active: true,
@@ -531,6 +608,42 @@ const PageContentDashboard: React.FC = () => {
{/* Home Tab */}
{activeTab === 'home' && (
+ {/* Seed Data Button */}
+
+
+
+ Quick Start
+ Load pre-configured luxury content to get started quickly
+
+
+
+
+
{/* Home Page Content Section */}
Home Page Content
@@ -559,14 +672,38 @@ const PageContentDashboard: React.FC = () => {
-
- setHomeData({ ...homeData, hero_image: e.target.value })}
- className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
- placeholder="https://example.com/hero-image.jpg"
- />
+
+
+ setHomeData({ ...homeData, hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {homeData.hero_image && (
+
+
+
+ )}
@@ -621,17 +758,1388 @@ const PageContentDashboard: React.FC = () => {
placeholder="SEO Meta Description"
/>
+
+
-
-
+ {/* Amenities Section */}
+
+ Amenities Section
+
+
+
+ setHomeData({ ...homeData, amenities_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Amenities"
+ />
+
+
+ setHomeData({ ...homeData, amenities_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience world-class amenities"
+ />
+
+
+
+ Amenities
+
+
+
+ {Array.isArray(homeData.amenities) && homeData.amenities.map((amenity, index) => (
+
+
+ Amenity {index + 1}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], icon: iconName };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], image: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {amenity?.image && (
+
+
+
+ )}
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentAmenities = Array.isArray(prevData.amenities) ? [...prevData.amenities] : [];
+ currentAmenities[index] = { ...currentAmenities[index], title: e.target.value };
+ return { ...prevData, amenities: currentAmenities };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.amenities || homeData.amenities.length === 0) && (
+ No amenities added yet. Click "Add Amenity" to get started.
+ )}
+
+
+
+ {/* Luxury Section */}
+
+ Luxury Section
+
+
+
+ setHomeData({ ...homeData, luxury_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Experience Unparalleled Luxury"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Where elegance meets comfort in every detail"
+ />
+
+
+
+
+ setHomeData({ ...homeData, luxury_section_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/luxury-image.jpg or upload"
+ />
+
+
+ {homeData.luxury_section_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Luxury Features Section */}
+
+
+ Luxury Features
+
+
+
+ {Array.isArray(homeData.luxury_features) && homeData.luxury_features.map((feature, index) => (
+
+
+ Luxury Feature {index + 1}
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], icon: iconName };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentFeatures = Array.isArray(prevData.luxury_features) ? [...prevData.luxury_features] : [];
+ currentFeatures[index] = { ...currentFeatures[index], title: e.target.value };
+ return { ...prevData, luxury_features: currentFeatures };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Feature Title"
+ />
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_features || homeData.luxury_features.length === 0) && (
+ No luxury features added yet. Click "Add Luxury Feature" to get started.
+ )}
+
+
+
+ {/* Luxury Gallery Section */}
+
+
+ Luxury Gallery
+
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Luxury Gallery"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_gallery_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Discover our exquisite spaces and amenities"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_gallery) && homeData.luxury_gallery.map((image, index) => (
+
+ {
+ setHomeData((prevData) => {
+ const currentGallery = Array.isArray(prevData.luxury_gallery) ? [...prevData.luxury_gallery] : [];
+ currentGallery[index] = e.target.value;
+ return { ...prevData, luxury_gallery: currentGallery };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {image && (
+
+
+
+ )}
+
+ ))}
+ {(!homeData.luxury_gallery || homeData.luxury_gallery.length === 0) && (
+ No gallery images added yet. Click "Add Gallery Image" to get started.
+ )}
+
+
+
+ {/* Luxury Testimonials Section */}
+
+
+ Luxury Testimonials
+
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Guest Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_testimonials_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Hear from our valued guests about their luxury stay"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_testimonials) && homeData.luxury_testimonials.map((testimonial, index) => (
+
+
+ Luxury Testimonial {index + 1}
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], name: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], title: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ />
+
+
+
+
+
+ {
+ setHomeData((prevData) => {
+ const currentTestimonials = Array.isArray(prevData.luxury_testimonials) ? [...prevData.luxury_testimonials] : [];
+ currentTestimonials[index] = { ...currentTestimonials[index], image: e.target.value };
+ return { ...prevData, luxury_testimonials: currentTestimonials };
+ });
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {testimonial?.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_testimonials || homeData.luxury_testimonials.length === 0) && (
+ No luxury testimonials added yet. Click "Add Luxury Testimonial" to get started.
+ )}
+
+
+
+ {/* About Preview Section */}
+
+ About Preview Section
+
+
+
+ setHomeData({ ...homeData, about_preview_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="About Our Hotel"
+ />
+
+
+
+ setHomeData({ ...homeData, about_preview_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ />
+
+
+
+
+
+
+
+ setHomeData({ ...homeData, about_preview_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/about-image.jpg or upload"
+ />
+
+
+ {homeData.about_preview_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Stats Section */}
+
+
+ Statistics Section
+
+
+
+ {Array.isArray(homeData.stats) && homeData.stats.map((stat, index) => (
+
+
+ Stat {index + 1}
+
+
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], number: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="1000+"
+ />
+
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], label: e.target.value };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Happy Guests"
+ />
+
+
+ {
+ const currentStats = Array.isArray(homeData.stats) ? [...homeData.stats] : [];
+ currentStats[index] = { ...currentStats[index], icon: iconName };
+ setHomeData({ ...homeData, stats: currentStats });
+ }}
+ label="Icon"
+ />
+
+
+
+ ))}
+ {(!homeData.stats || homeData.stats.length === 0) && (
+ No statistics added yet. Click "Add Stat" to get started.
+ )}
+
+
+
+ {/* Luxury Services Section */}
+
+
+ Luxury Services
+
+
+
+ {Array.isArray(homeData.luxury_services) && homeData.luxury_services.map((service, index) => (
+
+
+ Service {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {service?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_services) ? [...homeData.luxury_services] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_services: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Service Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_services || homeData.luxury_services.length === 0) && (
+ No services added yet. Click "Add Service" to get started.
+ )}
+
+
+
+ {/* Luxury Experiences Section */}
+
+
+ Luxury Experiences
+
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unique Experiences"
+ />
+
+
+
+ setHomeData({ ...homeData, luxury_experiences_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Unforgettable moments await"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.luxury_experiences) && homeData.luxury_experiences.map((experience, index) => (
+
+
+ Experience {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {experience?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.luxury_experiences) ? [...homeData.luxury_experiences] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, luxury_experiences: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Experience Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.luxury_experiences || homeData.luxury_experiences.length === 0) && (
+ No experiences added yet. Click "Add Experience" to get started.
+ )}
+
+
+
+ {/* Awards Section */}
+
+
+ Awards & Certifications
+
+
+
+
+ setHomeData({ ...homeData, awards_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Awards & Recognition"
+ />
+
+
+
+ setHomeData({ ...homeData, awards_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Recognized excellence in hospitality"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.awards) && homeData.awards.map((award, index) => (
+
+
+ Award {index + 1}
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], icon: iconName };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ label="Icon"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], year: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="2024"
+ />
+
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], image: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="URL or upload"
+ />
+
+
+ {award?.image && (
+
+
+
+ )}
+
+
+
+ {
+ const current = Array.isArray(homeData.awards) ? [...homeData.awards] : [];
+ current[index] = { ...current[index], title: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, awards: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Award Title"
+ />
+
+
+
+
+
+
+ ))}
+ {(!homeData.awards || homeData.awards.length === 0) && (
+ No awards added yet. Click "Add Award" to get started.
+ )}
+
+
+
+ {/* CTA Section */}
+
+ Call to Action Section
+
+
+
+ setHomeData({ ...homeData, cta_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Ready to Experience Luxury?"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book your stay today"
+ />
+
+
+
+
+ setHomeData({ ...homeData, cta_button_text: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Book Now"
+ />
+
+
+
+ setHomeData({ ...homeData, cta_button_link: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="/rooms"
+ />
+
+
+
+
+
+ setHomeData({ ...homeData, cta_image: e.target.value })}
+ className="flex-1 px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="https://example.com/cta-image.jpg or upload"
+ />
+
+
+ {homeData.cta_image && (
+
+
+
+ )}
+
+
+
+
+ {/* Partners Section */}
+
+
+ Partners & Brands
+
+
+
+
+ setHomeData({ ...homeData, partners_section_title: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Our Partners"
+ />
+
+
+
+ setHomeData({ ...homeData, partners_section_subtitle: e.target.value })}
+ className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl"
+ placeholder="Trusted by leading brands"
+ />
+
+
+
+
+
+
+ {Array.isArray(homeData.partners) && homeData.partners.map((partner, index) => (
+
+
+ Partner {index + 1}
+
+
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], name: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="Partner Name"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], logo: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com/logo.png"
+ />
+
+
+
+ {
+ const current = Array.isArray(homeData.partners) ? [...homeData.partners] : [];
+ current[index] = { ...current[index], link: e.target.value };
+ setHomeData((prevData) => ({ ...prevData, partners: current }));
+ }}
+ className="w-full px-4 py-2 border-2 border-gray-200 rounded-lg"
+ placeholder="https://example.com"
+ />
+
+
+
+ ))}
+ {(!homeData.partners || homeData.partners.length === 0) && (
+ No partners added yet. Click "Add Partner" to get started.
+ )}
+
+
+
+ {/* Save Button */}
+
+
+
@@ -696,8 +2204,11 @@ const PageContentDashboard: React.FC = () => {
{banner.title}
- {banner.link && (
- {banner.link}
+ {banner.description && (
+ {banner.description}
+ )}
+ {banner.link_url && (
+ {banner.link_url}
)}
@@ -781,12 +2292,24 @@ const PageContentDashboard: React.FC = () => {
/>
+
+
+
+
setBannerFormData({ ...bannerFormData, link: e.target.value })}
+ value={bannerFormData.link_url}
+ onChange={(e) => setBannerFormData({ ...bannerFormData, link_url: e.target.value })}
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
placeholder="https://example.com"
/>
@@ -1140,6 +2663,690 @@ const PageContentDashboard: React.FC = () => {
/>
+ {/* Hero Image */}
+
+
+
+ setAboutData({ ...aboutData, about_hero_image: e.target.value })}
+ className="flex-1 px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
+ placeholder="https://example.com/hero-image.jpg or upload"
+ />
+
+
+ {aboutData.about_hero_image && (
+
+
+
+ )}
+
+
+ {/* Mission */}
+
+
+
+
+ {/* Vision */}
+
+
+
+
+ {/* Values Section */}
+
+
+ Our Values
+
+
+ {Array.isArray(aboutData.values) && aboutData.values.length > 0 ? (
+
+ {aboutData.values.map((value, index) => (
+
+
+ Value {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], icon };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newValues = [...(prevData.values || [])];
+ newValues[index] = { ...newValues[index], title: e.target.value };
+ return { ...prevData, values: newValues };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Value title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No values added yet. Click "Add Value" to get started.
+ )}
+
+
+ {/* Features Section */}
+
+
+ Features
+
+
+ {Array.isArray(aboutData.features) && aboutData.features.length > 0 ? (
+
+ {aboutData.features.map((feature, index) => (
+
+
+ Feature {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], icon };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newFeatures = [...(prevData.features || [])];
+ newFeatures[index] = { ...newFeatures[index], title: e.target.value };
+ return { ...prevData, features: newFeatures };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Feature title"
+ />
+
+
+
+
+
+ ))}
+
+ ) : (
+ No features added yet. Click "Add Feature" to get started.
+ )}
+
+
+ {/* Team Section */}
+
+
+ Team Members
+
+
+ {Array.isArray(aboutData.team) && aboutData.team.length > 0 ? (
+
+ {aboutData.team.map((member, index) => (
+
+
+ Team Member {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], name: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Full name"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], role: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Job title"
+ />
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTeam = [...(prevData.team || [])];
+ newTeam[index] = { ...newTeam[index], image: e.target.value };
+ return { ...prevData, team: newTeam };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {member.image && (
+
+
+
+ )}
+
+
+
+
+
+ ))}
+
+ ) : (
+ No team members added yet. Click "Add Team Member" to get started.
+ )}
+
+
+ {/* Timeline Section */}
+
+
+ Timeline / History
+
+
+ {Array.isArray(aboutData.timeline) && aboutData.timeline.length > 0 ? (
+
+ {aboutData.timeline.map((event, index) => (
+
+
+ Event {index + 1}
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], year: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], title: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Event title"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newTimeline = [...(prevData.timeline || [])];
+ newTimeline[index] = { ...newTimeline[index], image: e.target.value };
+ return { ...prevData, timeline: newTimeline };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {event.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No timeline events added yet. Click "Add Event" to get started.
+ )}
+
+
+ {/* Achievements Section */}
+
+
+ Achievements & Awards
+
+
+ {Array.isArray(aboutData.achievements) && aboutData.achievements.length > 0 ? (
+
+ {aboutData.achievements.map((achievement, index) => (
+
+
+ Achievement {index + 1}
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], icon };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ />
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], title: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="Achievement title"
+ />
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], year: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="2020"
+ />
+
+
+
+
+
+
+
+
+ {
+ setAboutData((prevData) => {
+ const newAchievements = [...(prevData.achievements || [])];
+ newAchievements[index] = { ...newAchievements[index], image: e.target.value };
+ return { ...prevData, achievements: newAchievements };
+ });
+ }}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-lg"
+ placeholder="https://example.com/image.jpg or upload"
+ />
+
+
+ {achievement.image && (
+
+
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No achievements added yet. Click "Add Achievement" to get started.
+ )}
+
+
@@ -1400,6 +3607,28 @@ const PageContentDashboard: React.FC = () => {
+ {/* Copyright Text */}
+
+ Copyright Text
+
+
+
+ setFooterData({ ...footerData, copyright_text: e.target.value })}
+ className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-amber-400 focus:ring-4 focus:ring-amber-100 transition-all duration-200"
+ placeholder="© {YEAR} Luxury Hotel. All rights reserved."
+ />
+
+ The {`{YEAR}`} placeholder will be automatically replaced with the current year.
+
+
+
+
+
Get In Touch
- -+
We'd love to hear from you. Contact us for reservations or inquiries.
+
Address
-
+
{displayAddress
.split('\n').map((line, i) => (
+
+
+
+
{pageContent?.hero_title || 'Featured & Newest Rooms'}
-
+
{pageContent?.hero_subtitle || pageContent?.description || 'Discover our most popular accommodations and latest additions'}
{/* View All Rooms Button - Golden, Centered */} -- Easy Booking -
-- Search and book rooms with just a few clicks -
-+ {feature.title} +
+ )} + {feature.description && ( ++ {feature.description} +
+ )} ++ Easy Booking +
++ Search and book rooms with just a few clicks +
+- Best Prices -
-- Best price guarantee in the market -
-+ Best Prices +
++ Best price guarantee in the market +
++ 24/7 Support +
++ Support team always ready to serve +
+- 24/7 Support -
-- Support team always ready to serve +
+ {pageContent.luxury_section_title || 'Experience Unparalleled Luxury'} +
+ {pageContent.luxury_section_subtitle && ( ++ {pageContent.luxury_section_subtitle}
+ )} ++ {feature.title} +
++ {feature.description} +
++ {pageContent.luxury_gallery_section_title || 'Luxury Gallery'} +
+ {pageContent.luxury_gallery_section_subtitle && ( ++ {pageContent.luxury_gallery_section_subtitle} +
+ )} ++ {pageContent.luxury_testimonials_section_title || 'Guest Experiences'} +
+ {pageContent.luxury_testimonials_section_subtitle && ( ++ {pageContent.luxury_testimonials_section_subtitle} +
+ )} ++ Hear from our valued guests about their luxury stay +
+{testimonial.name}
+ {testimonial.title && ( +{testimonial.title}
+ )} +{testimonial.quote}
++ {pageContent.amenities_section_title || 'Luxury Amenities'} +
++ {pageContent.amenities_section_subtitle || 'Experience world-class amenities designed for your comfort'} +
++ {amenity.title} +
++ {amenity.description} +
++ {pageContent.testimonials_section_title || 'Guest Testimonials'} +
++ {pageContent.testimonials_section_subtitle || 'See what our guests say about their experience'} +
+{testimonial.name}
+{testimonial.role}
+"{testimonial.comment}"
++ {pageContent.about_preview_title || 'About Our Hotel'} +
+ {pageContent.about_preview_subtitle && ( ++ {pageContent.about_preview_subtitle} +
+ )} + {pageContent.about_preview_content && ( ++ {pageContent.about_preview_content} +
+ )} + + + Learn More ++ {pageContent.luxury_services_section_title || 'Luxury Services'} +
+ {pageContent.luxury_services_section_subtitle && ( ++ {pageContent.luxury_services_section_subtitle} +
+ )} ++ {service.title} +
++ {service.description} +
++ {pageContent.luxury_experiences_section_title || 'Unique Experiences'} +
+ {pageContent.luxury_experiences_section_subtitle && ( ++ {pageContent.luxury_experiences_section_subtitle} +
+ )} ++ {experience.title} +
++ {experience.description} +
++ {pageContent.awards_section_title || 'Awards & Recognition'} +
+ {pageContent.awards_section_subtitle && ( ++ {pageContent.awards_section_subtitle} +
+ )} ++ {award.title} +
+ {award.description && ( ++ {award.description} +
+ )} ++ {pageContent.cta_title} +
+ {pageContent.cta_subtitle && ( ++ {pageContent.cta_subtitle} +
+ )} + {pageContent.cta_button_text && pageContent.cta_button_link && ( + + + {pageContent.cta_button_text} ++ {pageContent.partners_section_title || 'Our Partners'} +
+ {pageContent.partners_section_subtitle && ( ++ {pageContent.partners_section_subtitle} +
+ )} +1. Search booking
-
+
+ Check-ins for {formatDate(selectedDate)}
+
+
+ {checkInBookings.length}
+
+ No check-ins scheduled for this date
+
+
+ Check-outs for {formatDate(selectedDate)}
+
+
+ {checkOutBookings.length}
+
+ No check-outs scheduled for this date
+
-
- 2. Booking Information
-
+
+
+ 2. Booking Information
+
+
+ - No booking selected -
-- Please enter booking number above to start check-in process -
-Manage and track all invoices
No invoices found
diff --git a/Frontend/src/pages/admin/PageContentDashboard.tsx b/Frontend/src/pages/admin/PageContentDashboard.tsx index d52089e8..d33edbec 100644 --- a/Frontend/src/pages/admin/PageContentDashboard.tsx +++ b/Frontend/src/pages/admin/PageContentDashboard.tsx @@ -7,13 +7,6 @@ import { Search, Save, Globe, - Facebook, - Twitter, - Instagram, - Linkedin, - Youtube, - MapPin, - Phone, X, Plus, Trash2, @@ -31,6 +24,8 @@ import { pageContentService, PageContent, PageType, UpdatePageContentData, banne import { toast } from 'react-toastify'; import Loading from '../../components/common/Loading'; import { ConfirmationDialog } from '../../components/common'; +import IconPicker from '../../components/admin/IconPicker'; +import { luxuryContentSeed } from '../../data/luxuryContentSeed'; type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo'; @@ -62,7 +57,7 @@ const PageContentDashboard: React.FC = () => { title: '', description: '', image_url: '', - link: '', + link_url: '', position: 'home', display_order: 0, is_active: true, @@ -113,6 +108,21 @@ const PageContentDashboard: React.FC = () => { } }; + // Helper function to normalize arrays (handle both arrays and JSON strings) + const normalizeArray = (value: any): any[] => { + if (!value) return []; + if (Array.isArray(value)) return value; + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + return []; + }; + const initializeFormData = (contents: RecordQuick Start
+Load pre-configured luxury content to get started quickly
+Home Page Content
@@ -559,14 +672,38 @@ const PageContentDashboard: React.FC = () => {Amenities Section
+Amenities
+ +Amenity {index + 1}
+ +No amenities added yet. Click "Add Amenity" to get started.
+ )} +Luxury Section
+Luxury Features
+ +Luxury Feature {index + 1}
+ +No luxury features added yet. Click "Add Luxury Feature" to get started.
+ )} +Luxury Gallery
+No gallery images added yet. Click "Add Gallery Image" to get started.
+ )} +Luxury Testimonials
+Luxury Testimonial {index + 1}
+ +No luxury testimonials added yet. Click "Add Luxury Testimonial" to get started.
+ )} +About Preview Section
+Statistics Section
+ +Stat {index + 1}
+ +No statistics added yet. Click "Add Stat" to get started.
+ )} +Luxury Services
+ +Service {index + 1}
+ +No services added yet. Click "Add Service" to get started.
+ )} +Luxury Experiences
+Experience {index + 1}
+ +No experiences added yet. Click "Add Experience" to get started.
+ )} +Awards & Certifications
+Award {index + 1}
+ +No awards added yet. Click "Add Award" to get started.
+ )} +Call to Action Section
+Partners & Brands
+Partner {index + 1}
+ +No partners added yet. Click "Add Partner" to get started.
+ )} +Our Values
+ +Value {index + 1}
+ +No values added yet. Click "Add Value" to get started.
+ )} +Features
+ +Feature {index + 1}
+ +No features added yet. Click "Add Feature" to get started.
+ )} +Team Members
+ +Team Member {index + 1}
+ +No team members added yet. Click "Add Team Member" to get started.
+ )} +Timeline / History
+ +Event {index + 1}
+ +No timeline events added yet. Click "Add Event" to get started.
+ )} +Achievements & Awards
+ +Achievement {index + 1}
+ +No achievements added yet. Click "Add Achievement" to get started.
+ )} +Copyright Text
++ The {`{YEAR}`} placeholder will be automatically replaced with the current year. +
+