Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,57 +14,31 @@ from urllib.parse import urlencode
|
|
| 14 |
logging.basicConfig(filename='appointment_bot.log', level=logging.INFO,
|
| 15 |
format='%(asctime)s - %(levelname)s - %(message)s')
|
| 16 |
|
| 17 |
-
# ===
|
| 18 |
-
SMTP_SERVER = "mail.mubashirdev.com"
|
| 19 |
-
SMTP_PORT = 465
|
| 20 |
-
SMTP_USERNAME = os.getenv("SMTP_USERNAME")
|
| 21 |
-
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
| 22 |
-
if not SMTP_USERNAME or not SMTP_PASSWORD:
|
| 23 |
-
raise Exception("SMTP_USERNAME and SMTP_PASSWORD environment variables are not set.")
|
| 24 |
-
|
| 25 |
-
FROM_EMAIL = SMTP_USERNAME
|
| 26 |
-
FROM_NAME = "Mubashir Hussain"
|
| 27 |
-
|
| 28 |
-
# === Excel File ===
|
| 29 |
EXCEL_FILE = "appointments.xlsx"
|
| 30 |
-
|
| 31 |
-
# === Time Slots ===
|
| 32 |
TIME_SLOTS = [f"{hour:02d}:00" for hour in range(9, 18)] # 9 AM to 5 PM
|
| 33 |
-
|
| 34 |
-
# === Date Options (Next 15 Days for Speed) ===
|
| 35 |
DATE_OPTIONS = [
|
| 36 |
(datetime.now() + timedelta(days=i)).strftime("%Y-%m-%d (%A)")
|
| 37 |
for i in range(0, 15)
|
| 38 |
]
|
| 39 |
-
|
| 40 |
-
# === Common Timezones (minimized for speed) ===
|
| 41 |
COMMON_TIMEZONES = [
|
| 42 |
"UTC", "Asia/Karachi", "US/Pacific", "US/Eastern",
|
| 43 |
"Europe/London", "Asia/Tokyo", "America/New_York", "Asia/Dubai"
|
| 44 |
]
|
| 45 |
|
| 46 |
-
# ===
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
"text": f"Appointment with {FROM_NAME}",
|
| 58 |
-
"details": f"Appointment for {name} ({email}). Contact: https://mubashirdev.com",
|
| 59 |
-
"dates": f"{start_time_utc}/{end_time_utc}",
|
| 60 |
-
"ctz": timezone
|
| 61 |
-
}
|
| 62 |
-
return f"https://www.google.com/calendar/render?{urlencode(event)}"
|
| 63 |
-
except Exception as e:
|
| 64 |
-
logging.error(f"Google Calendar link error: {e}")
|
| 65 |
-
raise Exception("Failed to generate calendar link.")
|
| 66 |
|
| 67 |
-
# ===
|
| 68 |
def send_email(to_email, subject, body):
|
| 69 |
try:
|
| 70 |
msg = MIMEMultipart()
|
|
@@ -73,58 +47,88 @@ def send_email(to_email, subject, body):
|
|
| 73 |
msg["Subject"] = subject
|
| 74 |
msg.attach(MIMEText(body, "plain"))
|
| 75 |
context = ssl.create_default_context()
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
logging.info(f"Email sent to {to_email}")
|
| 80 |
return True
|
| 81 |
except Exception as e:
|
| 82 |
logging.error(f"Email error: {e}")
|
| 83 |
raise Exception(f"Failed to send email: {str(e)}")
|
| 84 |
|
| 85 |
-
|
| 86 |
-
def save_to_excel(name, email, date_str, time_str, timezone, message):
|
| 87 |
try:
|
| 88 |
-
|
| 89 |
-
appointment_data = {
|
| 90 |
-
"Name": name,
|
| 91 |
-
"Email": email,
|
| 92 |
-
"Date": date_clean,
|
| 93 |
-
"Time": time_str,
|
| 94 |
-
"Timezone": timezone,
|
| 95 |
-
"Message": message or "No message",
|
| 96 |
-
"Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 97 |
-
}
|
| 98 |
-
df = pd.DataFrame([appointment_data])
|
| 99 |
mode = 'a' if os.path.exists(EXCEL_FILE) else 'w'
|
| 100 |
with pd.ExcelWriter(EXCEL_FILE, engine='openpyxl', mode=mode, if_sheet_exists='overlay' if mode == 'a' else None) as writer:
|
| 101 |
df.to_excel(writer, index=False, sheet_name='Sheet1', header=not os.path.exists(EXCEL_FILE))
|
| 102 |
-
logging.info(f"Appointment saved: {
|
| 103 |
return True
|
| 104 |
except Exception as e:
|
| 105 |
logging.error(f"Excel save error: {e}")
|
| 106 |
raise Exception(f"Failed to save appointment: {str(e)}")
|
| 107 |
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
def book_appointment(name, email, date, time, timezone, message):
|
| 110 |
try:
|
|
|
|
| 111 |
if not all([name, email, date, time, timezone]):
|
| 112 |
raise Exception("Please fill out all required fields.")
|
| 113 |
|
| 114 |
date_clean = date.split(" ")[0] # Extract YYYY-MM-DD
|
| 115 |
-
datetime.strptime(date_clean, "%Y-%m-%d")
|
| 116 |
tz = pytz.timezone(timezone)
|
| 117 |
appointment_time = datetime.strptime(f"{date_clean} {time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
| 118 |
if appointment_time < datetime.now(tz):
|
| 119 |
raise Exception("Appointments must be in the future.")
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
raise Exception("Failed to generate calendar link.")
|
| 127 |
|
|
|
|
| 128 |
admin_msg = f"New Appointment:\nName: {name}\nEmail: {email}\nDate: {date_clean}\nTime: {time}\nTimezone: {timezone}\nMessage: {message or 'No message'}"
|
| 129 |
send_email("admin@mubashirdev.com", "📅 New Appointment", admin_msg)
|
| 130 |
|
|
@@ -139,18 +143,18 @@ def book_appointment(name, email, date, time, timezone, message):
|
|
| 139 |
"datasets": [{
|
| 140 |
"label": "Appointment",
|
| 141 |
"data": [1],
|
| 142 |
-
"backgroundColor": "#
|
| 143 |
-
"borderColor": "#
|
| 144 |
"borderWidth": 2
|
| 145 |
}]
|
| 146 |
},
|
| 147 |
"options": {
|
| 148 |
"scales": {
|
| 149 |
-
"y": {"beginAtZero": True, "ticks": {"stepSize": 1, "color": "#
|
| 150 |
-
"x": {"title": {"display": True, "text": "Date & Time", "color": "#
|
| 151 |
},
|
| 152 |
"plugins": {
|
| 153 |
-
"title": {"display": True, "text": f"Appointment for {name}", "color": "#
|
| 154 |
}
|
| 155 |
}
|
| 156 |
}
|
|
@@ -164,14 +168,14 @@ def book_appointment(name, email, date, time, timezone, message):
|
|
| 164 |
logging.error(f"Booking error: {str(e)}")
|
| 165 |
return str(e), []
|
| 166 |
|
| 167 |
-
# ===
|
| 168 |
JS_ENHANCEMENT = """
|
| 169 |
<script>
|
| 170 |
document.addEventListener('DOMContentLoaded', function() {
|
| 171 |
const dateDropdown = document.querySelector('select[id*="date"]');
|
| 172 |
if (dateDropdown) {
|
| 173 |
-
dateDropdown.style.background = '#
|
| 174 |
-
dateDropdown.style.border = '2px solid #
|
| 175 |
dateDropdown.style.borderRadius = '8px';
|
| 176 |
dateDropdown.style.padding = '12px';
|
| 177 |
dateDropdown.style.fontSize = '16px';
|
|
@@ -179,7 +183,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 179 |
Array.from(dateDropdown.options).forEach(option => {
|
| 180 |
if (option.value.includes(today)) {
|
| 181 |
option.style.fontWeight = 'bold';
|
| 182 |
-
option.style.color = '#
|
| 183 |
}
|
| 184 |
});
|
| 185 |
}
|
|
@@ -192,33 +196,34 @@ with gr.Blocks(theme=gr.themes.Soft(), css="""
|
|
| 192 |
.gradio-container {
|
| 193 |
max-width: 700px;
|
| 194 |
margin: 20px auto;
|
| 195 |
-
background:
|
| 196 |
padding: 24px;
|
| 197 |
border-radius: 16px;
|
| 198 |
-
box-shadow: 0 4px 16px rgba(0,0,0,0.
|
| 199 |
}
|
| 200 |
.welcome {
|
| 201 |
text-align: center;
|
| 202 |
font-size: 28px;
|
| 203 |
-
color: #
|
| 204 |
margin: 0 0 20px;
|
| 205 |
font-weight: 700;
|
| 206 |
}
|
| 207 |
.gr-textbox, .gr-dropdown {
|
| 208 |
border-radius: 8px;
|
| 209 |
-
border: 2px solid #
|
| 210 |
padding: 12px;
|
| 211 |
font-size: 16px;
|
| 212 |
-
background:
|
|
|
|
| 213 |
transition: border-color 0.2s;
|
| 214 |
}
|
| 215 |
.gr-textbox:focus, .gr-dropdown:focus {
|
| 216 |
-
border-color: #
|
| 217 |
outline: none;
|
| 218 |
}
|
| 219 |
.gr-button {
|
| 220 |
-
background: #
|
| 221 |
-
color:
|
| 222 |
font-size: 16px;
|
| 223 |
font-weight: 600;
|
| 224 |
padding: 12px 32px;
|
|
@@ -229,26 +234,26 @@ with gr.Blocks(theme=gr.themes.Soft(), css="""
|
|
| 229 |
transition: background 0.2s;
|
| 230 |
}
|
| 231 |
.gr-button:hover {
|
| 232 |
-
background: #
|
| 233 |
}
|
| 234 |
.gr-textbox[label="Result"] {
|
| 235 |
-
background: #
|
| 236 |
-
border-color: #
|
| 237 |
-
color: #
|
| 238 |
font-weight: 500;
|
| 239 |
padding: 16px;
|
| 240 |
border-radius: 8px;
|
| 241 |
}
|
| 242 |
.gr-column {
|
| 243 |
-
background:
|
| 244 |
padding: 20px;
|
| 245 |
border-radius: 12px;
|
| 246 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.
|
| 247 |
}
|
| 248 |
.footer {
|
| 249 |
text-align: center;
|
| 250 |
font-size: 14px;
|
| 251 |
-
color: #
|
| 252 |
margin-top: 20px;
|
| 253 |
}
|
| 254 |
.logo {
|
|
@@ -258,7 +263,18 @@ with gr.Blocks(theme=gr.themes.Soft(), css="""
|
|
| 258 |
}
|
| 259 |
.required::after {
|
| 260 |
content: " *";
|
| 261 |
-
color: #D32F2F;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
}
|
| 263 |
@media (max-width: 600px) {
|
| 264 |
.gradio-container {
|
|
@@ -277,7 +293,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css="""
|
|
| 277 |
f"""
|
| 278 |
<img src="https://ik.imagekit.io/kqeykkpy5/WhatsApp_Image_2024-12-20_at_02.57.39-removebg-preview.png?updatedAt=1747857266922" class="logo" alt="Mubashir Hussain Logo">
|
| 279 |
<div class="welcome">Schedule Your Appointment</div>
|
| 280 |
-
<p
|
| 281 |
{JS_ENHANCEMENT}
|
| 282 |
"""
|
| 283 |
)
|
|
@@ -294,7 +310,7 @@ with gr.Blocks(theme=gr.themes.Soft(), css="""
|
|
| 294 |
|
| 295 |
gr.Markdown(
|
| 296 |
"""
|
| 297 |
-
<div class="footer">Powered by <a href="https://mubashirdev.com"
|
| 298 |
"""
|
| 299 |
)
|
| 300 |
|
|
|
|
| 14 |
logging.basicConfig(filename='appointment_bot.log', level=logging.INFO,
|
| 15 |
format='%(asctime)s - %(levelname)s - %(message)s')
|
| 16 |
|
| 17 |
+
# === Constants ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
EXCEL_FILE = "appointments.xlsx"
|
|
|
|
|
|
|
| 19 |
TIME_SLOTS = [f"{hour:02d}:00" for hour in range(9, 18)] # 9 AM to 5 PM
|
|
|
|
|
|
|
| 20 |
DATE_OPTIONS = [
|
| 21 |
(datetime.now() + timedelta(days=i)).strftime("%Y-%m-%d (%A)")
|
| 22 |
for i in range(0, 15)
|
| 23 |
]
|
|
|
|
|
|
|
| 24 |
COMMON_TIMEZONES = [
|
| 25 |
"UTC", "Asia/Karachi", "US/Pacific", "US/Eastern",
|
| 26 |
"Europe/London", "Asia/Tokyo", "America/New_York", "Asia/Dubai"
|
| 27 |
]
|
| 28 |
|
| 29 |
+
# === Email Configuration ===
|
| 30 |
+
SMTP_SERVER = "mail.mubashirdev.com"
|
| 31 |
+
SMTP_PORT = 465
|
| 32 |
+
SMTP_USERNAME = os.getenv("SMTP_USERNAME")
|
| 33 |
+
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
| 34 |
+
FROM_NAME = "Mubashir Hussain"
|
| 35 |
+
|
| 36 |
+
# Validate environment variables
|
| 37 |
+
if not SMTP_USERNAME or not SMTP_PASSWORD:
|
| 38 |
+
raise Exception("SMTP_USERNAME and SMTP_PASSWORD environment variables are not set.")
|
| 39 |
+
FROM_EMAIL = SMTP_USERNAME
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
# === Utility Functions ===
|
| 42 |
def send_email(to_email, subject, body):
|
| 43 |
try:
|
| 44 |
msg = MIMEMultipart()
|
|
|
|
| 47 |
msg["Subject"] = subject
|
| 48 |
msg.attach(MIMEText(body, "plain"))
|
| 49 |
context = ssl.create_default_context()
|
| 50 |
+
|
| 51 |
+
# Try custom SMTP server first
|
| 52 |
+
try:
|
| 53 |
+
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context, timeout=5) as server:
|
| 54 |
+
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
| 55 |
+
server.send_message(msg)
|
| 56 |
+
except Exception as e:
|
| 57 |
+
logging.warning(f"Custom SMTP failed: {e}. Falling back to Gmail.")
|
| 58 |
+
# Fallback to Gmail
|
| 59 |
+
with smtplib.SMTP("smtp.gmail.com", 587) as server:
|
| 60 |
+
server.starttls(context=context)
|
| 61 |
+
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
| 62 |
+
server.send_message(msg)
|
| 63 |
+
|
| 64 |
logging.info(f"Email sent to {to_email}")
|
| 65 |
return True
|
| 66 |
except Exception as e:
|
| 67 |
logging.error(f"Email error: {e}")
|
| 68 |
raise Exception(f"Failed to send email: {str(e)}")
|
| 69 |
|
| 70 |
+
def save_to_excel(data):
|
|
|
|
| 71 |
try:
|
| 72 |
+
df = pd.DataFrame([data])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
mode = 'a' if os.path.exists(EXCEL_FILE) else 'w'
|
| 74 |
with pd.ExcelWriter(EXCEL_FILE, engine='openpyxl', mode=mode, if_sheet_exists='overlay' if mode == 'a' else None) as writer:
|
| 75 |
df.to_excel(writer, index=False, sheet_name='Sheet1', header=not os.path.exists(EXCEL_FILE))
|
| 76 |
+
logging.info(f"Appointment saved: {data['Name']}, {data['Email']}")
|
| 77 |
return True
|
| 78 |
except Exception as e:
|
| 79 |
logging.error(f"Excel save error: {e}")
|
| 80 |
raise Exception(f"Failed to save appointment: {str(e)}")
|
| 81 |
|
| 82 |
+
def create_calendar_link(name, email, date, time, timezone):
|
| 83 |
+
try:
|
| 84 |
+
tz = pytz.timezone(timezone)
|
| 85 |
+
start_time = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
| 86 |
+
end_time = start_time + timedelta(hours=1)
|
| 87 |
+
start_time_utc = start_time.astimezone(pytz.UTC).strftime("%Y%m%dT%H%M%SZ")
|
| 88 |
+
end_time_utc = end_time.astimezone(pytz.UTC).strftime("%Y%m%dT%H%M%SZ")
|
| 89 |
+
event = {
|
| 90 |
+
"action": "TEMPLATE",
|
| 91 |
+
"text": f"Appointment with {FROM_NAME}",
|
| 92 |
+
"details": f"Appointment for {name} ({email}). Contact: https://mubashirdev.com",
|
| 93 |
+
"dates": f"{start_time_utc}/{end_time_utc}",
|
| 94 |
+
"ctz": timezone
|
| 95 |
+
}
|
| 96 |
+
return f"https://www.google.com/calendar/render?{urlencode(event)}"
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logging.error(f"Calendar link error: {e}")
|
| 99 |
+
raise Exception("Failed to generate calendar link.")
|
| 100 |
+
|
| 101 |
+
# === Core Logic ===
|
| 102 |
def book_appointment(name, email, date, time, timezone, message):
|
| 103 |
try:
|
| 104 |
+
# Input validation
|
| 105 |
if not all([name, email, date, time, timezone]):
|
| 106 |
raise Exception("Please fill out all required fields.")
|
| 107 |
|
| 108 |
date_clean = date.split(" ")[0] # Extract YYYY-MM-DD
|
|
|
|
| 109 |
tz = pytz.timezone(timezone)
|
| 110 |
appointment_time = datetime.strptime(f"{date_clean} {time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
| 111 |
if appointment_time < datetime.now(tz):
|
| 112 |
raise Exception("Appointments must be in the future.")
|
| 113 |
|
| 114 |
+
# Prepare data
|
| 115 |
+
data = {
|
| 116 |
+
"Name": name,
|
| 117 |
+
"Email": email,
|
| 118 |
+
"Date": date_clean,
|
| 119 |
+
"Time": time,
|
| 120 |
+
"Timezone": timezone,
|
| 121 |
+
"Message": message or "No message",
|
| 122 |
+
"Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
# Save to Excel
|
| 126 |
+
save_to_excel(data)
|
| 127 |
|
| 128 |
+
# Generate calendar link
|
| 129 |
+
calendar_link = create_calendar_link(name, email, date_clean, time, timezone)
|
|
|
|
| 130 |
|
| 131 |
+
# Send emails
|
| 132 |
admin_msg = f"New Appointment:\nName: {name}\nEmail: {email}\nDate: {date_clean}\nTime: {time}\nTimezone: {timezone}\nMessage: {message or 'No message'}"
|
| 133 |
send_email("admin@mubashirdev.com", "📅 New Appointment", admin_msg)
|
| 134 |
|
|
|
|
| 143 |
"datasets": [{
|
| 144 |
"label": "Appointment",
|
| 145 |
"data": [1],
|
| 146 |
+
"backgroundColor": "#4E342E",
|
| 147 |
+
"borderColor": "#3E2723",
|
| 148 |
"borderWidth": 2
|
| 149 |
}]
|
| 150 |
},
|
| 151 |
"options": {
|
| 152 |
"scales": {
|
| 153 |
+
"y": {"beginAtZero": True, "ticks": {"stepSize": 1, "color": "#FFFFFF"}, "title": {"display": True, "text": "Confirmed", "color": "#FFFFFF"}},
|
| 154 |
+
"x": {"title": {"display": True, "text": "Date & Time", "color": "#FFFFFF"}, "ticks": {"color": "#FFFFFF"}}
|
| 155 |
},
|
| 156 |
"plugins": {
|
| 157 |
+
"title": {"display": True, "text": f"Appointment for {name}", "color": "#FFFFFF", "font": {"size": 18}}
|
| 158 |
}
|
| 159 |
}
|
| 160 |
}
|
|
|
|
| 168 |
logging.error(f"Booking error: {str(e)}")
|
| 169 |
return str(e), []
|
| 170 |
|
| 171 |
+
# === UI Enhancements ===
|
| 172 |
JS_ENHANCEMENT = """
|
| 173 |
<script>
|
| 174 |
document.addEventListener('DOMContentLoaded', function() {
|
| 175 |
const dateDropdown = document.querySelector('select[id*="date"]');
|
| 176 |
if (dateDropdown) {
|
| 177 |
+
dateDropdown.style.background = '#FFFFFF';
|
| 178 |
+
dateDropdown.style.border = '2px solid #8D6E63';
|
| 179 |
dateDropdown.style.borderRadius = '8px';
|
| 180 |
dateDropdown.style.padding = '12px';
|
| 181 |
dateDropdown.style.fontSize = '16px';
|
|
|
|
| 183 |
Array.from(dateDropdown.options).forEach(option => {
|
| 184 |
if (option.value.includes(today)) {
|
| 185 |
option.style.fontWeight = 'bold';
|
| 186 |
+
option.style.color = '#4E342E';
|
| 187 |
}
|
| 188 |
});
|
| 189 |
}
|
|
|
|
| 196 |
.gradio-container {
|
| 197 |
max-width: 700px;
|
| 198 |
margin: 20px auto;
|
| 199 |
+
background: #5D4037; /* Brown background */
|
| 200 |
padding: 24px;
|
| 201 |
border-radius: 16px;
|
| 202 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
| 203 |
}
|
| 204 |
.welcome {
|
| 205 |
text-align: center;
|
| 206 |
font-size: 28px;
|
| 207 |
+
color: #FFFFFF; /* White text */
|
| 208 |
margin: 0 0 20px;
|
| 209 |
font-weight: 700;
|
| 210 |
}
|
| 211 |
.gr-textbox, .gr-dropdown {
|
| 212 |
border-radius: 8px;
|
| 213 |
+
border: 2px solid #8D6E63; /* Light brown border */
|
| 214 |
padding: 12px;
|
| 215 |
font-size: 16px;
|
| 216 |
+
background: #FFFFFF; /* White background */
|
| 217 |
+
color: #4E342E; /* Dark brown text */
|
| 218 |
transition: border-color 0.2s;
|
| 219 |
}
|
| 220 |
.gr-textbox:focus, .gr-dropdown:focus {
|
| 221 |
+
border-color: #4E342E; /* Darker brown on focus */
|
| 222 |
outline: none;
|
| 223 |
}
|
| 224 |
.gr-button {
|
| 225 |
+
background: #4E342E; /* Dark brown button */
|
| 226 |
+
color: #FFFFFF; /* White text */
|
| 227 |
font-size: 16px;
|
| 228 |
font-weight: 600;
|
| 229 |
padding: 12px 32px;
|
|
|
|
| 234 |
transition: background 0.2s;
|
| 235 |
}
|
| 236 |
.gr-button:hover {
|
| 237 |
+
background: #6D4C41; /* Lighter brown on hover */
|
| 238 |
}
|
| 239 |
.gr-textbox[label="Result"] {
|
| 240 |
+
background: #EFEBE9; /* Soft beige for success */
|
| 241 |
+
border-color: #8D6E63; /* Light brown border */
|
| 242 |
+
color: #4E342E; /* Dark brown text */
|
| 243 |
font-weight: 500;
|
| 244 |
padding: 16px;
|
| 245 |
border-radius: 8px;
|
| 246 |
}
|
| 247 |
.gr-column {
|
| 248 |
+
background: #FFFFFF; /* White background for form container */
|
| 249 |
padding: 20px;
|
| 250 |
border-radius: 12px;
|
| 251 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 252 |
}
|
| 253 |
.footer {
|
| 254 |
text-align: center;
|
| 255 |
font-size: 14px;
|
| 256 |
+
color: #FFFFFF; /* White text */
|
| 257 |
margin-top: 20px;
|
| 258 |
}
|
| 259 |
.logo {
|
|
|
|
| 263 |
}
|
| 264 |
.required::after {
|
| 265 |
content: " *";
|
| 266 |
+
color: #D32F2F; /* Red for required fields */
|
| 267 |
+
}
|
| 268 |
+
p {
|
| 269 |
+
text-align: center;
|
| 270 |
+
color: #FFFFFF !important; /* White text for instructions */
|
| 271 |
+
}
|
| 272 |
+
a {
|
| 273 |
+
color: #EFEBE9 !important; /* Soft beige for links */
|
| 274 |
+
text-decoration: none;
|
| 275 |
+
}
|
| 276 |
+
a:hover {
|
| 277 |
+
text-decoration: underline;
|
| 278 |
}
|
| 279 |
@media (max-width: 600px) {
|
| 280 |
.gradio-container {
|
|
|
|
| 293 |
f"""
|
| 294 |
<img src="https://ik.imagekit.io/kqeykkpy5/WhatsApp_Image_2024-12-20_at_02.57.39-removebg-preview.png?updatedAt=1747857266922" class="logo" alt="Mubashir Hussain Logo">
|
| 295 |
<div class="welcome">Schedule Your Appointment</div>
|
| 296 |
+
<p>Select a date and time below to book with Mubashir Hussain. You'll receive a confirmation email with a Google Calendar link.</p>
|
| 297 |
{JS_ENHANCEMENT}
|
| 298 |
"""
|
| 299 |
)
|
|
|
|
| 310 |
|
| 311 |
gr.Markdown(
|
| 312 |
"""
|
| 313 |
+
<div class="footer">Powered by <a href="https://mubashirdev.com">mubashirdev.com</a></div>
|
| 314 |
"""
|
| 315 |
)
|
| 316 |
|