Spaces:
Sleeping
Sleeping
✨ UI/UX: Stabilize button actions and add Laurier Cares bags celebration
Browse filesStart/End Visit
- Replace st.spinner with stable placeholders to prevent page jitter
- Add success status messages and brief, controlled reruns
- Remove layout shaking on click by using min-heights and containers
Login Celebration
- Replace balloons with custom purple ‘Laurier Cares’ grocery bags animation
- Lightweight CSS + inline JS; no external assets; auto-cleanup overlay
Global UX
- Spinner CSS overrides; higher z-index; smoother transitions
- Reuse ModernUIComponents status helpers for consistent feedback
No functional logic changes to data flow; purely UX improvements.
- streamlit_app.py +53 -15
- ui_improvements.py +54 -0
streamlit_app.py
CHANGED
|
@@ -285,7 +285,34 @@ def auth_block() -> tuple[bool, Optional[str]]:
|
|
| 285 |
|
| 286 |
log_event("login_success", email, {"method": "otp"})
|
| 287 |
st.success("🎉 Welcome back! Your shift has started.")
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
# Small delay before rerun for better UX
|
| 291 |
time.sleep(1)
|
|
@@ -434,9 +461,12 @@ def main():
|
|
| 434 |
col1, col2, col3 = st.columns([1, 1, 1])
|
| 435 |
|
| 436 |
with col1:
|
| 437 |
-
if not active_visit
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
| 440 |
payload = {
|
| 441 |
"visit_code": fallback_visit_code(),
|
| 442 |
"started_at": datetime.utcnow().isoformat(),
|
|
@@ -447,27 +477,35 @@ def main():
|
|
| 447 |
if not v.get("visit_code"):
|
| 448 |
v["visit_code"] = payload["visit_code"]
|
| 449 |
st.session_state["active_visit"] = v
|
| 450 |
-
|
|
|
|
| 451 |
st.info(f"**Visit Code:** `{v['visit_code']}`")
|
| 452 |
log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
|
|
|
|
| 453 |
st.rerun()
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
| 457 |
|
| 458 |
with col2:
|
| 459 |
-
if active_visit
|
| 460 |
-
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
| 462 |
sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
|
| 463 |
.eq("id", active_visit["id"]).execute()
|
| 464 |
-
st.
|
| 465 |
log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
|
| 466 |
st.session_state.pop("active_visit", None)
|
|
|
|
| 467 |
st.rerun()
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
|
|
|
| 471 |
|
| 472 |
with col3:
|
| 473 |
if st.session_state.get("active_visit"):
|
|
|
|
| 285 |
|
| 286 |
log_event("login_success", email, {"method": "otp"})
|
| 287 |
st.success("🎉 Welcome back! Your shift has started.")
|
| 288 |
+
# Custom Laurier Cares bags celebration
|
| 289 |
+
bags_html = """
|
| 290 |
+
<div class="bags-overlay" id="bags-overlay">
|
| 291 |
+
<!-- 12 bags across random x positions -->
|
| 292 |
+
</div>
|
| 293 |
+
<script>
|
| 294 |
+
(function(){
|
| 295 |
+
const overlay = document.getElementById('bags-overlay');
|
| 296 |
+
if (!overlay) return;
|
| 297 |
+
const count = 12;
|
| 298 |
+
for (let i = 0; i < count; i++) {
|
| 299 |
+
const bag = document.createElement('div');
|
| 300 |
+
bag.className = 'bag';
|
| 301 |
+
bag.style.left = Math.random()*100 + 'vw';
|
| 302 |
+
bag.style.animationDelay = (Math.random()*0.8).toFixed(2)+'s';
|
| 303 |
+
bag.style.transform = `translateY(-140px) rotate(${(Math.random()*10-5).toFixed(1)}deg)`;
|
| 304 |
+
const label = document.createElement('div');
|
| 305 |
+
label.className = 'label';
|
| 306 |
+
label.textContent = 'Laurier\nCares';
|
| 307 |
+
bag.appendChild(label);
|
| 308 |
+
overlay.appendChild(bag);
|
| 309 |
+
}
|
| 310 |
+
setTimeout(()=>{ overlay.classList.add('fade-out'); }, 2400);
|
| 311 |
+
setTimeout(()=>{ overlay.remove(); }, 3000);
|
| 312 |
+
})();
|
| 313 |
+
</script>
|
| 314 |
+
"""
|
| 315 |
+
st.markdown(bags_html, unsafe_allow_html=True)
|
| 316 |
|
| 317 |
# Small delay before rerun for better UX
|
| 318 |
time.sleep(1)
|
|
|
|
| 461 |
col1, col2, col3 = st.columns([1, 1, 1])
|
| 462 |
|
| 463 |
with col1:
|
| 464 |
+
if not active_visit:
|
| 465 |
+
visit_status = st.empty()
|
| 466 |
+
if st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
|
| 467 |
+
try:
|
| 468 |
+
with visit_status.container():
|
| 469 |
+
st.markdown(ModernUIComponents.create_status_message("Creating visit...", "loading"), unsafe_allow_html=True)
|
| 470 |
payload = {
|
| 471 |
"visit_code": fallback_visit_code(),
|
| 472 |
"started_at": datetime.utcnow().isoformat(),
|
|
|
|
| 477 |
if not v.get("visit_code"):
|
| 478 |
v["visit_code"] = payload["visit_code"]
|
| 479 |
st.session_state["active_visit"] = v
|
| 480 |
+
visit_status.empty()
|
| 481 |
+
st.markdown(ModernUIComponents.create_status_message(f"Visit #{v['id']} started", "success"), unsafe_allow_html=True)
|
| 482 |
st.info(f"**Visit Code:** `{v['visit_code']}`")
|
| 483 |
log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
|
| 484 |
+
time.sleep(0.4)
|
| 485 |
st.rerun()
|
| 486 |
+
except Exception as e:
|
| 487 |
+
visit_status.empty()
|
| 488 |
+
st.error(f"❌ Could not start visit: {e}")
|
| 489 |
+
log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
|
| 490 |
|
| 491 |
with col2:
|
| 492 |
+
if active_visit:
|
| 493 |
+
end_status = st.empty()
|
| 494 |
+
if st.button("🏁 End Visit", use_container_width=True, type="secondary"):
|
| 495 |
+
try:
|
| 496 |
+
with end_status.container():
|
| 497 |
+
st.markdown(ModernUIComponents.create_status_message("Ending visit...", "loading"), unsafe_allow_html=True)
|
| 498 |
sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
|
| 499 |
.eq("id", active_visit["id"]).execute()
|
| 500 |
+
st.markdown(ModernUIComponents.create_status_message("Visit completed successfully", "success"), unsafe_allow_html=True)
|
| 501 |
log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
|
| 502 |
st.session_state.pop("active_visit", None)
|
| 503 |
+
time.sleep(0.3)
|
| 504 |
st.rerun()
|
| 505 |
+
except Exception as e:
|
| 506 |
+
end_status.empty()
|
| 507 |
+
st.error(f"❌ Could not end visit: {e}")
|
| 508 |
+
log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
|
| 509 |
|
| 510 |
with col3:
|
| 511 |
if st.session_state.get("active_visit"):
|
ui_improvements.py
CHANGED
|
@@ -594,6 +594,60 @@ class ModernUIComponents:
|
|
| 594 |
white-space: nowrap;
|
| 595 |
border: 0;
|
| 596 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
|
| 598 |
/* Focus states for accessibility */
|
| 599 |
button:focus,
|
|
|
|
| 594 |
white-space: nowrap;
|
| 595 |
border: 0;
|
| 596 |
}
|
| 597 |
+
|
| 598 |
+
/* Laurier Cares Bags Celebration Animation */
|
| 599 |
+
.bags-overlay {
|
| 600 |
+
position: fixed;
|
| 601 |
+
inset: 0;
|
| 602 |
+
pointer-events: none;
|
| 603 |
+
z-index: 9999;
|
| 604 |
+
overflow: hidden;
|
| 605 |
+
}
|
| 606 |
+
.bag {
|
| 607 |
+
position: absolute;
|
| 608 |
+
top: -120px;
|
| 609 |
+
width: 80px;
|
| 610 |
+
height: 100px;
|
| 611 |
+
background: linear-gradient(180deg, #7C3AED, #5B21B6);
|
| 612 |
+
border-radius: 8px 8px 12px 12px;
|
| 613 |
+
box-shadow: var(--shadow-lg);
|
| 614 |
+
border: 2px solid rgba(255,255,255,0.2);
|
| 615 |
+
animation: bagDrop 2.8s ease-in forwards;
|
| 616 |
+
display: flex;
|
| 617 |
+
align-items: center;
|
| 618 |
+
justify-content: center;
|
| 619 |
+
color: white;
|
| 620 |
+
font-weight: 700;
|
| 621 |
+
font-size: 11px;
|
| 622 |
+
text-align: center;
|
| 623 |
+
padding: 6px;
|
| 624 |
+
}
|
| 625 |
+
.bag:before {
|
| 626 |
+
content: "";
|
| 627 |
+
position: absolute;
|
| 628 |
+
top: -14px;
|
| 629 |
+
left: 22px;
|
| 630 |
+
width: 36px;
|
| 631 |
+
height: 22px;
|
| 632 |
+
border: 3px solid rgba(255,255,255,0.7);
|
| 633 |
+
border-bottom: none;
|
| 634 |
+
border-radius: 18px 18px 0 0;
|
| 635 |
+
}
|
| 636 |
+
.bag .label {
|
| 637 |
+
font-size: 10px;
|
| 638 |
+
line-height: 1.1;
|
| 639 |
+
letter-spacing: 0.3px;
|
| 640 |
+
}
|
| 641 |
+
@keyframes bagDrop {
|
| 642 |
+
0% { transform: translateY(-140px) rotate(0deg); opacity: 0; }
|
| 643 |
+
10% { opacity: 1; }
|
| 644 |
+
80% { transform: translateY(100vh) rotate(10deg); }
|
| 645 |
+
100% { transform: translateY(110vh) rotate(14deg); opacity: 0; }
|
| 646 |
+
}
|
| 647 |
+
.bags-overlay.fade-out { animation: overlayFade 0.4s ease forwards; }
|
| 648 |
+
@keyframes overlayFade {
|
| 649 |
+
to { opacity: 0; }
|
| 650 |
+
}
|
| 651 |
|
| 652 |
/* Focus states for accessibility */
|
| 653 |
button:focus,
|