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 |
-
|
| 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 |
-
<
|
| 722 |
-
|
| 723 |
-
<div
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
<
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
</div>
|
| 751 |
-
);
|
| 752 |
-
})}
|
| 753 |
-
</div>
|
| 754 |
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
|
|
|
|
|
|
|
|
|
| 770 |
</div>
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
</div>
|
| 774 |
-
</div>
|
| 775 |
-
)}
|
| 776 |
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
);
|
| 783 |
}
|
| 784 |
|
|
@@ -808,7 +854,7 @@ function DailyLimitCell({ ms }: { ms?: MetaStatus }) {
|
|
| 808 |
</div>
|
| 809 |
</div>
|
| 810 |
</div>
|
| 811 |
-
|
| 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 |
}
|