focustiki commited on
Commit
dcd4161
·
1 Parent(s): 1f26b21

✨ UI/UX: Stabilize button actions and add Laurier Cares bags celebration

Browse files

Start/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.

Files changed (2) hide show
  1. streamlit_app.py +53 -15
  2. 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
- st.balloons()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
438
- try:
439
- with st.spinner("Creating visit..."):
 
 
 
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
- st.success(f"✅ Visit #{v['id']} started")
 
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
- except Exception as e:
455
- st.error(f"❌ Could not start visit: {e}")
456
- log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
 
457
 
458
  with col2:
459
- if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
460
- try:
461
- with st.spinner("Ending visit..."):
 
 
 
462
  sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
463
  .eq("id", active_visit["id"]).execute()
464
- st.success("Visit completed successfully")
465
  log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
466
  st.session_state.pop("active_visit", None)
 
467
  st.rerun()
468
- except Exception as e:
469
- st.error(f"❌ Could not end visit: {e}")
470
- log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
 
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,