| import gradio as gr |
| import pandas as pd |
| from datetime import datetime |
|
|
| |
| custom_css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600;700&family=Montserrat:wght@300;400;500;600&display=swap'); |
|
|
| :root { |
| --gold: #D4AF37; |
| --gold-dark: #B8941F; |
| --navy: #0A1828; |
| --navy-light: #1A2F42; |
| --cream: #F8F6F0; |
| --white: #FFFFFF; |
| --shadow: rgba(10, 24, 40, 0.15); |
| --shadow-heavy: rgba(10, 24, 40, 0.3); |
| } |
| |
| .gradio-container { |
| background: linear-gradient(135deg, #0A1828 0%, #1A2F42 50%, #0A1828 100%) !important; |
| font-family: 'Montserrat', sans-serif !important; |
| position: relative; |
| overflow-x: hidden; |
| } |
|
|
| .gradio-container::before { |
| content: ''; |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-image: |
| radial-gradient(circle at 20% 30%, rgba(212, 175, 55, 0.05) 0%, transparent 50%), |
| radial-gradient(circle at 80% 70%, rgba(212, 175, 55, 0.03) 0%, transparent 50%); |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| /* Header Styling */ |
| .luxury-header { |
| text-align: center; |
| padding: 60px 20px 40px; |
| position: relative; |
| z-index: 1; |
| } |
| |
| .luxury-header h1 { |
| font-family: 'Cormorant Garamond', serif !important; |
| font-size: 4.5em !important; |
| font-weight: 300 !important; |
| color: var(--cream) !important; |
| margin: 0 0 15px 0 !important; |
| letter-spacing: 3px !important; |
| text-transform: uppercase; |
| animation: fadeInDown 1s ease-out; |
| } |
| |
| .luxury-header .subtitle { |
| font-size: 1.1em; |
| color: var(--gold); |
| letter-spacing: 4px; |
| text-transform: uppercase; |
| font-weight: 300; |
| margin-top: 10px; |
| animation: fadeInUp 1s ease-out 0.3s both; |
| } |
| |
| .luxury-header .divider { |
| width: 100px; |
| height: 2px; |
| background: linear-gradient(90deg, transparent, var(--gold), transparent); |
| margin: 25px auto; |
| animation: expandWidth 1.2s ease-out 0.5s both; |
| } |
| |
| @keyframes fadeInDown { |
| from { |
| opacity: 0; |
| transform: translateY(-30px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| @keyframes fadeInUp { |
| from { |
| opacity: 0; |
| transform: translateY(30px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| @keyframes expandWidth { |
| from { |
| width: 0; |
| } |
| to { |
| width: 100px; |
| } |
| } |
| |
| /* Promotion Banner */ |
| .promo-banner { |
| background: linear-gradient(135deg, var(--gold-dark) 0%, var(--gold) 100%); |
| color: var(--navy); |
| padding: 35px; |
| border-radius: 0; |
| margin: 30px 0; |
| text-align: center; |
| position: relative; |
| overflow: hidden; |
| box-shadow: 0 10px 40px var(--shadow-heavy); |
| animation: slideInFromTop 0.8s ease-out; |
| } |
|
|
| .promo-banner::before { |
| content: ''; |
| position: absolute; |
| top: -50%; |
| left: -50%; |
| width: 200%; |
| height: 200%; |
| background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.15) 50%, transparent 70%); |
| animation: shimmer 3s infinite; |
| } |
| |
| @keyframes shimmer { |
| 0% { |
| transform: translateX(-100%) translateY(-100%) rotate(45deg); |
| } |
| 100% { |
| transform: translateX(100%) translateY(100%) rotate(45deg); |
| } |
| } |
| |
| @keyframes slideInFromTop { |
| from { |
| opacity: 0; |
| transform: translateY(-50px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .promo-banner .promo-title { |
| font-family: 'Cormorant Garamond', serif; |
| font-size: 1.4em; |
| font-weight: 600; |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| margin-bottom: 10px; |
| } |
| |
| .promo-banner .promo-message { |
| font-size: 2em; |
| font-weight: 600; |
| margin: 15px 0; |
| font-family: 'Cormorant Garamond', serif; |
| } |
| |
| .promo-banner .promo-time { |
| font-size: 0.85em; |
| opacity: 0.8; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| } |
| |
| /* Tab Styling */ |
| .tabitem { |
| background: rgba(248, 246, 240, 0.05) !important; |
| border: none !important; |
| border-radius: 15px !important; |
| padding: 40px !important; |
| margin-top: 20px !important; |
| backdrop-filter: blur(10px); |
| } |
| |
| button.selected { |
| background: var(--gold) !important; |
| color: var(--navy) !important; |
| font-weight: 600 !important; |
| border: none !important; |
| letter-spacing: 1px !important; |
| } |
| |
| .tabs button { |
| color: var(--cream) !important; |
| font-size: 1em !important; |
| padding: 15px 35px !important; |
| border: 1px solid rgba(212, 175, 55, 0.3) !important; |
| background: transparent !important; |
| transition: all 0.3s ease !important; |
| text-transform: uppercase; |
| letter-spacing: 1.5px; |
| font-weight: 500; |
| } |
|
|
| .tabs button:hover { |
| background: rgba(212, 175, 55, 0.1) !important; |
| border-color: var(--gold) !important; |
| } |
| |
| /* Hotel Cards - Luxury Table Styling */ |
| .dataframe { |
| background: var(--cream) !important; |
| border-radius: 15px !important; |
| overflow: hidden !important; |
| box-shadow: 0 15px 50px var(--shadow-heavy) !important; |
| font-family: 'Montserrat', sans-serif !important; |
| border: 1px solid rgba(212, 175, 55, 0.2) !important; |
| } |
| |
| .dataframe thead { |
| background: linear-gradient(135deg, var(--navy) 0%, var(--navy-light) 100%) !important; |
| } |
| |
| .dataframe thead th { |
| color: var(--gold) !important; |
| font-weight: 600 !important; |
| text-transform: uppercase !important; |
| letter-spacing: 1.5px !important; |
| padding: 20px !important; |
| font-size: 0.85em !important; |
| border: none !important; |
| } |
| |
| .dataframe tbody td { |
| padding: 25px 20px !important; |
| color: var(--navy) !important; |
| font-size: 0.95em !important; |
| border-bottom: 1px solid rgba(10, 24, 40, 0.08) !important; |
| transition: all 0.3s ease; |
| } |
| |
| .dataframe tbody tr { |
| transition: all 0.3s ease; |
| } |
|
|
| .dataframe tbody tr:hover { |
| background: rgba(212, 175, 55, 0.08) !important; |
| transform: scale(1.01); |
| } |
| |
| /* Input Styling */ |
| .gr-box, |
| input, |
| textarea, |
| select { |
| background: rgba(248, 246, 240, 0.95) !important; |
| border: 1px solid rgba(212, 175, 55, 0.3) !important; |
| color: var(--navy) !important; |
| border-radius: 8px !important; |
| transition: all 0.3s ease !important; |
| font-family: 'Montserrat', sans-serif !important; |
| } |
|
|
| .gr-box:focus, |
| input:focus, |
| textarea:focus, |
| select:focus { |
| border-color: var(--gold) !important; |
| box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.15) !important; |
| outline: none !important; |
| } |
| |
| label { |
| color: var(--cream) !important; |
| font-weight: 500 !important; |
| text-transform: uppercase !important; |
| letter-spacing: 1px !important; |
| font-size: 0.85em !important; |
| margin-bottom: 8px !important; |
| } |
| |
| /* Radio Buttons */ |
| .gr-radio { |
| background: transparent !important; |
| } |
| |
| .gr-radio label { |
| color: var(--cream) !important; |
| padding: 12px 20px !important; |
| border: 1px solid rgba(212, 175, 55, 0.3) !important; |
| border-radius: 8px !important; |
| margin: 5px !important; |
| transition: all 0.3s ease !important; |
| cursor: pointer !important; |
| text-transform: none !important; |
| letter-spacing: 0.5px !important; |
| } |
|
|
| .gr-radio label:hover { |
| background: rgba(212, 175, 55, 0.1) !important; |
| border-color: var(--gold) !important; |
| } |
|
|
| .gr-radio input:checked + label { |
| background: var(--gold) !important; |
| color: var(--navy) !important; |
| border-color: var(--gold) !important; |
| font-weight: 600 !important; |
| } |
| |
| /* Buttons */ |
| .gr-button { |
| background: linear-gradient(135deg, var(--gold-dark) 0%, var(--gold) 100%) !important; |
| color: var(--navy) !important; |
| border: none !important; |
| padding: 15px 35px !important; |
| font-weight: 600 !important; |
| text-transform: uppercase !important; |
| letter-spacing: 2px !important; |
| border-radius: 8px !important; |
| transition: all 0.4s ease !important; |
| box-shadow: 0 5px 20px rgba(212, 175, 55, 0.3) !important; |
| font-size: 0.9em !important; |
| position: relative !important; |
| overflow: hidden !important; |
| } |
|
|
| .gr-button::before { |
| content: ''; |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| width: 0; |
| height: 0; |
| border-radius: 50%; |
| background: rgba(255, 255, 255, 0.3); |
| transform: translate(-50%, -50%); |
| transition: width 0.6s, height 0.6s; |
| } |
|
|
| .gr-button:hover::before { |
| width: 300px; |
| height: 300px; |
| } |
|
|
| .gr-button:hover { |
| transform: translateY(-2px) !important; |
| box-shadow: 0 8px 30px rgba(212, 175, 55, 0.5) !important; |
| } |
| |
| .gr-button-secondary { |
| background: transparent !important; |
| border: 2px solid var(--gold) !important; |
| color: var(--gold) !important; |
| } |
|
|
| .gr-button-secondary:hover { |
| background: var(--gold) !important; |
| color: var(--navy) !important; |
| } |
| |
| /* Info Box */ |
| .info-box { |
| background: rgba(212, 175, 55, 0.1); |
| border-left: 4px solid var(--gold); |
| padding: 20px 25px; |
| border-radius: 0 8px 8px 0; |
| color: var(--cream); |
| margin: 20px 0; |
| font-size: 0.95em; |
| line-height: 1.6; |
| backdrop-filter: blur(5px); |
| } |
| |
| .info-box strong { |
| color: var(--gold); |
| font-weight: 600; |
| } |
| |
| /* Dashboard Cards */ |
| .dashboard-section { |
| background: rgba(248, 246, 240, 0.05); |
| border: 1px solid rgba(212, 175, 55, 0.2); |
| border-radius: 15px; |
| padding: 30px; |
| margin: 20px 0; |
| backdrop-filter: blur(10px); |
| } |
| |
| .dashboard-title { |
| font-family: 'Cormorant Garamond', serif; |
| font-size: 1.8em; |
| color: var(--gold); |
| margin-bottom: 10px; |
| font-weight: 600; |
| letter-spacing: 1px; |
| } |
| |
| .dashboard-subtitle { |
| color: var(--cream); |
| opacity: 0.8; |
| font-size: 0.9em; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| margin-bottom: 25px; |
| } |
| |
| /* Footer */ |
| .luxury-footer { |
| text-align: center; |
| margin-top: 80px; |
| padding: 40px 20px; |
| border-top: 1px solid rgba(212, 175, 55, 0.2); |
| color: var(--cream); |
| opacity: 0.6; |
| font-size: 0.85em; |
| letter-spacing: 1.5px; |
| text-transform: uppercase; |
| } |
| |
| /* Status Badge */ |
| .status-available { |
| color: #2ECC71; |
| font-weight: 600; |
| } |
| |
| .status-occupied { |
| color: #E74C3C; |
| font-weight: 600; |
| } |
| |
| /* Markdown Styling */ |
| .markdown-text { |
| color: var(--cream) !important; |
| line-height: 1.8 !important; |
| } |
| |
| .markdown-text h3 { |
| font-family: 'Cormorant Garamond', serif !important; |
| color: var(--gold) !important; |
| font-size: 1.6em !important; |
| margin-bottom: 15px !important; |
| font-weight: 600 !important; |
| } |
| |
| .markdown-text p { |
| color: var(--cream) !important; |
| opacity: 0.9 !important; |
| } |
| |
| /* Success Message */ |
| .success-message { |
| background: linear-gradient(135deg, rgba(46, 204, 113, 0.2) 0%, rgba(46, 204, 113, 0.1) 100%); |
| border: 1px solid rgba(46, 204, 113, 0.5); |
| color: #2ECC71; |
| padding: 20px; |
| border-radius: 8px; |
| text-align: center; |
| font-weight: 600; |
| letter-spacing: 1px; |
| animation: slideInFromRight 0.5s ease-out; |
| } |
| |
| @keyframes slideInFromRight { |
| from { |
| opacity: 0; |
| transform: translateX(30px); |
| } |
| to { |
| opacity: 1; |
| transform: translateX(0); |
| } |
| } |
| |
| /* Responsive Design */ |
| @media (max-width: 768px) { |
| .luxury-header h1 { |
| font-size: 2.5em !important; |
| } |
| |
| .promo-banner .promo-message { |
| font-size: 1.4em; |
| } |
| |
| .tabs button { |
| padding: 12px 20px !important; |
| font-size: 0.85em !important; |
| } |
| } |
|
|
| /* Scrollbar Styling */ |
| ::-webkit-scrollbar { |
| width: 10px; |
| } |
|
|
| ::-webkit-scrollbar-track { |
| background: var(--navy); |
| } |
|
|
| ::-webkit-scrollbar-thumb { |
| background: var(--gold); |
| border-radius: 5px; |
| } |
|
|
| ::-webkit-scrollbar-thumb:hover { |
| background: var(--gold-dark); |
| } |
| """ |
| |
| # --- ENHANCED DATA --- |
| data = { |
| "π¨ Hotel Name": [ |
| "Coral Reef View Resort", |
| "Male' Grand Stay Hotel", |
| "Villi Blue Inn", |
| "Azure Lagoon Suites", |
| "Paradise Isle Boutique" |
| ], |
| "π Location": [ |
| "Hulhumale Phase 1", |
| "Male' City Center", |
| "Villingili Island", |
| "Hulhumale Phase 2", |
| "Male' Waterfront" |
| ], |
| "π° Price/Night": [ |
| "850 MVR", |
| "1,100 MVR", |
| "650 MVR", |
| "950 MVR", |
| "1,250 MVR" |
| ], |
| "β Rating": [ |
| "4.8/5.0", |
| "4.6/5.0", |
| "4.5/5.0", |
| "4.9/5.0", |
| "4.7/5.0" |
| ], |
| "β
Status": [ |
| "Available", |
| "Available", |
| "Occupied", |
| "Available", |
| "Available" |
| ], |
| "π Direct Contact": [ |
| "+960 777-1234", |
| "+960 778-5678", |
| "+960 779-9101", |
| "+960 780-2345", |
| "+960 781-6789" |
| ] |
| } |
| df = pd.DataFrame(data) |
| |
| # --- FUNCTIONS --- |
| def filter_hotels(location): |
| if location == "All": |
| return df |
| elif location == "Male' City": |
| filtered = df[df["π Location"].str.contains("Male'", case=False, na=False)] |
| elif location == "Hulhumale": |
| filtered = df[df["π Location"].str.contains("Hulhumale", case=False, na=False)] |
| elif location == "Villingili": |
| filtered = df[df["π Location"].str.contains("Villingili", case=False, na=False)] |
| else: |
| filtered = df |
| return filtered |
| |
| def refresh_data(): |
| return df, "β¨ Availability refreshed successfully!" |
| |
| def send_promotion(hotel, message): |
| if not hotel or not message: |
| return gr.update(visible=False), "β οΈ Please select a hotel and enter a promotion message." |
| |
| time_now = datetime.now().strftime("%I:%M %p") |
| date_now = datetime.now().strftime("%B %d, %Y") |
| |
| promo_html = f""" |
| <div class='promo-banner'> |
| <div class='promo-title'>β‘ Exclusive Flash Offer</div> |
| <div class='promo-message'>{message}</div> |
| <div style='margin: 15px 0; font-size: 1.1em; font-weight: 600;'>π {hotel}</div> |
| <div class='promo-time'>Posted {time_now} β’ {date_now} β’ Valid Tonight Only</div> |
| </div> |
| """ |
| |
| return gr.update(value=promo_html, visible=True), f"<div class='success-message'>β
Promotion successfully broadcast to all users!</div>" |
| |
| def update_listing(hotel, status, price): |
| if not hotel: |
| return "β οΈ Please select your property first." |
| |
| success_msg = f""" |
| <div class='success-message'> |
| β
<strong>{hotel}</strong> updated successfully!<br> |
| Status: {status} | Price: {price} MVR/night |
| </div> |
| """ |
| return success_msg |
| |
| # --- UI LAYOUT --- |
| with gr.Blocks(css=custom_css, title="Hulhumale Luxury Hotel Direct", theme=gr.themes.Base()) as demo: |
| |
| |
| gr.HTML(""" |
| <div class='luxury-header'> |
| <h1>HULHUMALE DIRECT</h1> |
| <div class='divider'></div> |
| <div class='subtitle'>Luxury Accommodations β’ Direct Booking β’ Zero Commission</div> |
| </div> |
| """) |
| |
| # Promotion Display (Hidden by default) |
| promo_display = gr.HTML(visible=False) |
| |
| with gr.Tabs() as tabs: |
| |
| |
| with gr.Tab("π Discover Your Stay"): |
| |
| gr.HTML("<div class='dashboard-section'>") |
| |
| with gr.Row(): |
| filter_location = gr.Radio( |
| choices=["All", "Male' City", "Hulhumale", "Villingili"], |
| value="All", |
| label="Filter by Destination", |
| elem_classes="location-filter" |
| ) |
| |
| room_table = gr.DataFrame( |
| value=df, |
| interactive=False, |
| label="Available Properties", |
| wrap=True |
| ) |
| |
| gr.HTML(""" |
| <div class='info-box'> |
| <strong>π How to Book:</strong> Contact hotels directly using the numbers listed above. |
| Speak with owners personally, negotiate rates, and enjoy commission-free bookings. |
| All properties are verified and family-operated. |
| </div> |
| """) |
| |
| with gr.Row(): |
| refresh_btn = gr.Button("π Refresh Availability", variant="secondary", size="lg") |
| refresh_status = gr.Markdown("") |
| |
| gr.HTML("</div>") |
| |
| # --- OWNER TAB --- |
| with gr.Tab("βοΈ Property Management Portal"): |
| |
| gr.HTML(""" |
| <div class='dashboard-section'> |
| <div class='dashboard-title'>Welcome to Your Dashboard</div> |
| <div class='dashboard-subtitle'>Premium Client Portal β’ 350 MVR per Month</div> |
| </div> |
| """) |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.HTML("<div class='dashboard-section'>") |
| gr.Markdown("### π Update Your Listing") |
| |
| owner_hotel = gr.Dropdown( |
| choices=list(data["π¨ Hotel Name"]), |
| label="Select Your Property", |
| value=None |
| ) |
| |
| new_status = gr.Radio( |
| choices=["Available", "Occupied"], |
| label="Current Availability Status", |
| value="Available" |
| ) |
| |
| update_price = gr.Textbox( |
| label="Tonight's Rate (MVR)", |
| placeholder="e.g., 850", |
| value="" |
| ) |
| |
| update_btn = gr.Button("πΎ Update Live Listing", variant="primary", size="lg") |
| update_status = gr.HTML("") |
| |
| gr.HTML("</div>") |
| |
| with gr.Column(scale=1): |
| gr.HTML("<div class='dashboard-section'>") |
| gr.Markdown("### π£ Marketing & Promotions") |
| |
| gr.HTML(""" |
| <div class='info-box'> |
| Broadcast instant promotions to all active users browsing the platform. |
| Perfect for last-minute deals, early bird specials, or flash sales. |
| </div> |
| """) |
| |
| promo_hotel = gr.Dropdown( |
| choices=list(data["π¨ Hotel Name"]), |
| label="Property Name", |
| value=None |
| ) |
| |
| promo_text = gr.Textbox( |
| label="Promotion Message", |
| placeholder="e.g., 20% OFF for airport arrivals before midnight!", |
| lines=3 |
| ) |
| |
| promo_btn = gr.Button("π Broadcast Promotion", variant="primary", size="lg") |
| promo_status = gr.HTML("") |
| |
| gr.HTML("</div>") |
| |
| # --- EVENT HANDLERS --- |
| filter_location.change( |
| fn=filter_hotels, |
| inputs=[filter_location], |
| outputs=[room_table] |
| ) |
| |
| refresh_btn.click( |
| fn=refresh_data, |
| inputs=[], |
| outputs=[room_table, refresh_status] |
| ) |
| |
| update_btn.click( |
| fn=update_listing, |
| inputs=[owner_hotel, new_status, update_price], |
| outputs=[update_status] |
| ) |
| |
| promo_btn.click( |
| fn=send_promotion, |
| inputs=[promo_hotel, promo_text], |
| outputs=[promo_display, promo_status] |
| ) |
| |
| # Luxury Footer |
| gr.HTML(""" |
| <div class='luxury-footer'> |
| <div style='margin-bottom: 10px;'>β¦ β¦ β¦</div> |
| Hulhumale Direct β’ Connecting Travelers With Local Hospitality Since 2024 |
| <div style='margin-top: 10px; font-size: 0.75em; opacity: 0.5;'> |
| A Premium Local Marketplace |
| </div> |
| </div> |
| """) |
| |
| # Launch the app |
| if __name__ == "__main__": |
| demo.launch(share=False) |