CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
74aa3a1
·
1 Parent(s): 68eaf84

feat(admin): responsive bottom sheet + spring animation for TierInfoModal

Browse files

- Mobile: slides up from bottom (spring, damping 32), rounded-t-3xl,
drag handle pill, max 85dvh with internal scroll + overscroll-contain
- Desktop (sm+): centered card with same spring entrance
- AnimatePresence handles exit animation (slides back down)
- ESC key closes + body scroll lock during open
- Backdrop separated from panel div for correct z-layering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

apps/admin/src/pages/ClientsManagementView.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import { useState, useEffect } from 'react';
 
2
  import { Building2, Plus, MessageSquare, ShieldCheck, Activity, Loader2, X, RefreshCw, CheckCircle2, XCircle, AlertTriangle, Info } from 'lucide-react';
3
  import { api } from '../lib/api';
4
  import { useAuth } from '../lib/auth';
@@ -710,75 +711,120 @@ function BillingTierDisplay({ ms }: { ms?: MetaStatus }) {
710
  </button>
711
  )}
712
  </div>
713
- {showInfo && <TierInfoModal ms={ms} onClose={() => setShowInfo(false)} />}
714
  </>
715
  );
716
  }
717
 
718
- function TierInfoModal({ ms, onClose }: { ms?: MetaStatus; onClose: () => void }) {
719
  const currentTierOrder = TIER_CONFIG[ms?.messagingLimitTier ?? '']?.order ?? -1;
 
 
 
 
 
 
 
 
 
 
 
 
720
  return (
721
- <div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm flex items-center justify-center p-4 z-[200]" onClick={onClose}>
722
- <div className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 space-y-5" onClick={e => e.stopPropagation()}>
723
- <div className="flex items-center justify-between">
724
- <h3 className="font-bold text-slate-900 text-base">Limite de messagerie WhatsApp</h3>
725
- <button onClick={onClose} className="p-1.5 hover:bg-slate-100 rounded-lg transition">
726
- <X className="w-4 h-4 text-slate-500" />
727
- </button>
728
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
- {/* Tier ladder */}
731
- <div className="space-y-1.5">
732
- <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Niveaux de capacité Meta</p>
733
- {TIERS_IN_ORDER.map(({ key, label, desc }, i) => {
734
- const isCurrent = key === ms?.messagingLimitTier;
735
- const isPast = i < currentTierOrder;
736
- const cfg = TIER_CONFIG[key];
737
- return (
738
- <div key={key} className={`flex items-start gap-3 p-3 rounded-xl transition-all ${
739
- isCurrent ? 'ring-2 ring-indigo-200 bg-indigo-50' : isPast ? 'opacity-35' : 'opacity-55'
740
- }`}>
741
- <span className={`w-2 h-2 rounded-full mt-1.5 shrink-0 ${isCurrent ? (cfg?.dotCls ?? 'bg-indigo-400') : 'bg-slate-300'}`}
742
- style={{ background: isCurrent ? undefined : undefined }} />
743
- <div className="flex-1 min-w-0">
744
- <div className="flex items-center gap-2">
745
- <span className={`text-sm font-semibold ${isCurrent ? cfg?.textCls : 'text-slate-500'}`}>{label}</span>
746
- {isCurrent && <span className="text-[10px] font-bold bg-indigo-100 text-indigo-600 px-2 py-0.5 rounded-full">Actuel</span>}
747
- </div>
748
- <p className="text-[11px] text-slate-400 mt-0.5 leading-snug">{desc}</p>
749
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  </div>
751
- );
752
- })}
753
- </div>
754
 
755
- {/* Quality rating */}
756
- {ms?.qualityRating && ms.qualityRating !== 'UNKNOWN' && (
757
- <div className="space-y-2">
758
- <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Score Qualité du numéro</p>
759
- <div className="flex gap-2">
760
- {(['GREEN', 'YELLOW', 'RED'] as const).map(r => {
761
- const isCurrent = ms.qualityRating === r;
762
- const q = QUALITY_INFO[r];
763
- return (
764
- <div key={r} className={`flex-1 p-3 rounded-xl text-center transition-all ${
765
- isCurrent ? `ring-2 ring-offset-1 ${q.ringCls}` : 'bg-slate-50 opacity-35'
766
- }`}>
767
- <span className={`inline-block w-3 h-3 rounded-full ${q.dotCls} mb-1`} />
768
- <p className="text-xs font-bold text-slate-700">{q.label}</p>
769
- <p className="text-[10px] text-slate-500 mt-0.5 leading-snug">{q.desc}</p>
 
 
 
770
  </div>
771
- );
772
- })}
773
- </div>
774
- </div>
775
- )}
776
 
777
- <p className="text-[11px] text-slate-400 text-center">
778
- Meta met à jour ces données automatiquement. Les niveaux progressent sans action de votre part.
779
- </p>
780
- </div>
781
- </div>
 
 
 
 
782
  );
783
  }
784
 
@@ -808,7 +854,7 @@ function DailyLimitCell({ ms }: { ms?: MetaStatus }) {
808
  </div>
809
  </div>
810
  </div>
811
- {showInfo && <TierInfoModal ms={ms} onClose={() => setShowInfo(false)} />}
812
  </>
813
  );
814
  }
 
1
  import { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
  import { Building2, Plus, MessageSquare, ShieldCheck, Activity, Loader2, X, RefreshCw, CheckCircle2, XCircle, AlertTriangle, Info } from 'lucide-react';
4
  import { api } from '../lib/api';
5
  import { useAuth } from '../lib/auth';
 
711
  </button>
712
  )}
713
  </div>
714
+ <TierInfoModal ms={ms} open={showInfo} onClose={() => setShowInfo(false)} />
715
  </>
716
  );
717
  }
718
 
719
+ function TierInfoModal({ ms, open, onClose }: { ms?: MetaStatus; open: boolean; onClose: () => void }) {
720
  const currentTierOrder = TIER_CONFIG[ms?.messagingLimitTier ?? '']?.order ?? -1;
721
+
722
+ useEffect(() => {
723
+ if (!open) return;
724
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
725
+ document.addEventListener('keydown', onKey);
726
+ document.body.style.overflow = 'hidden';
727
+ return () => {
728
+ document.removeEventListener('keydown', onKey);
729
+ document.body.style.overflow = '';
730
+ };
731
+ }, [open, onClose]);
732
+
733
  return (
734
+ <AnimatePresence>
735
+ {open && (
736
+ <motion.div
737
+ className="fixed inset-0 z-[200] flex items-end sm:items-center justify-center sm:p-4"
738
+ initial={{ opacity: 0 }}
739
+ animate={{ opacity: 1 }}
740
+ exit={{ opacity: 0 }}
741
+ transition={{ duration: 0.2 }}
742
+ onClick={onClose}
743
+ >
744
+ {/* Backdrop */}
745
+ <div className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm" />
746
+
747
+ {/* Panel — bottom sheet on mobile, centered card on desktop */}
748
+ <motion.div
749
+ className="relative bg-white w-full sm:max-w-md sm:rounded-2xl rounded-t-3xl shadow-2xl flex flex-col"
750
+ style={{ maxHeight: '85dvh' }}
751
+ initial={{ y: '100%' }}
752
+ animate={{ y: 0 }}
753
+ exit={{ y: '100%' }}
754
+ transition={{ type: 'spring', damping: 32, stiffness: 320, mass: 0.8 }}
755
+ onClick={e => e.stopPropagation()}
756
+ >
757
+ {/* Drag handle — mobile only */}
758
+ <div className="flex justify-center pt-3 pb-0 sm:hidden shrink-0">
759
+ <div className="w-9 h-1 bg-slate-200 rounded-full" />
760
+ </div>
761
 
762
+ {/* Sticky header */}
763
+ <div className="flex items-center justify-between px-6 pt-4 pb-3 sm:pt-6 shrink-0">
764
+ <h3 className="font-bold text-slate-900 text-base">Limite de messagerie WhatsApp</h3>
765
+ <button onClick={onClose} className="p-1.5 hover:bg-slate-100 rounded-lg transition -mr-1">
766
+ <X className="w-4 h-4 text-slate-500" />
767
+ </button>
768
+ </div>
769
+
770
+ {/* Scrollable body */}
771
+ <div className="overflow-y-auto overscroll-contain px-6 pb-8 space-y-5">
772
+
773
+ {/* Tier ladder */}
774
+ <div className="space-y-1.5">
775
+ <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Niveaux de capacité Meta</p>
776
+ {TIERS_IN_ORDER.map(({ key, label, desc }, i) => {
777
+ const isCurrent = key === ms?.messagingLimitTier;
778
+ const isPast = i < currentTierOrder;
779
+ const cfg = TIER_CONFIG[key];
780
+ return (
781
+ <div key={key} className={`flex items-start gap-3 p-3 rounded-xl transition-all ${
782
+ isCurrent ? 'ring-2 ring-indigo-200 bg-indigo-50' : isPast ? 'opacity-30' : 'opacity-50'
783
+ }`}>
784
+ <span className={`w-2 h-2 rounded-full mt-1.5 shrink-0 ${isCurrent ? (cfg?.dotCls ?? 'bg-indigo-400') : 'bg-slate-300'}`} />
785
+ <div className="flex-1 min-w-0">
786
+ <div className="flex items-center gap-2 flex-wrap">
787
+ <span className={`text-sm font-semibold ${isCurrent ? cfg?.textCls : 'text-slate-500'}`}>{label}</span>
788
+ {isCurrent && <span className="text-[10px] font-bold bg-indigo-100 text-indigo-600 px-2 py-0.5 rounded-full">Actuel</span>}
789
+ </div>
790
+ <p className="text-[11px] text-slate-400 mt-0.5 leading-snug">{desc}</p>
791
+ </div>
792
+ </div>
793
+ );
794
+ })}
795
  </div>
 
 
 
796
 
797
+ {/* Quality rating */}
798
+ {ms?.qualityRating && ms.qualityRating !== 'UNKNOWN' && (
799
+ <div className="space-y-2">
800
+ <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Score Qualité du numéro</p>
801
+ <div className="flex gap-2">
802
+ {(['GREEN', 'YELLOW', 'RED'] as const).map(r => {
803
+ const isCurrent = ms.qualityRating === r;
804
+ const q = QUALITY_INFO[r];
805
+ return (
806
+ <div key={r} className={`flex-1 p-3 rounded-xl text-center transition-all ${
807
+ isCurrent ? `ring-2 ring-offset-1 ${q.ringCls}` : 'bg-slate-50 opacity-30'
808
+ }`}>
809
+ <span className={`inline-block w-3 h-3 rounded-full ${q.dotCls} mb-1.5`} />
810
+ <p className="text-xs font-bold text-slate-700">{q.label}</p>
811
+ <p className="text-[10px] text-slate-500 mt-0.5 leading-snug">{q.desc}</p>
812
+ </div>
813
+ );
814
+ })}
815
  </div>
816
+ </div>
817
+ )}
 
 
 
818
 
819
+ <p className="text-[11px] text-slate-400 text-center leading-relaxed">
820
+ Meta met à jour ces données automatiquement.<br />
821
+ Les niveaux progressent sans action de votre part.
822
+ </p>
823
+ </div>
824
+ </motion.div>
825
+ </motion.div>
826
+ )}
827
+ </AnimatePresence>
828
  );
829
  }
830
 
 
854
  </div>
855
  </div>
856
  </div>
857
+ <TierInfoModal ms={ms} open={showInfo} onClose={() => setShowInfo(false)} />
858
  </>
859
  );
860
  }