fix: Implement responsive Drawer for payment gateway settings on mobile

Problem: Payment gateway settings modal was using Dialog on all screen sizes
Solution: Split into responsive Dialog (desktop) and Drawer (mobile)

Changes:
1. Added Drawer and useMediaQuery imports
2. Added isDesktop hook: useMediaQuery("(min-width: 768px)")
3. Split modal into two conditional renders:
   - Desktop (≥768px): Dialog with horizontal footer layout
   - Mobile (<768px): Drawer with vertical footer layout

Desktop Layout (Dialog):
- Center modal overlay
- Horizontal footer: Cancel | View in WC | Save
- max-h-[80vh] for scrolling

Mobile Layout (Drawer):
- Bottom sheet (slides up from bottom)
- Vertical footer (full width buttons):
  1. Save Settings (primary)
  2. View in WooCommerce (ghost)
  3. Cancel (outline)
- max-h-[90vh] for more screen space
- Swipe down to dismiss

Benefits:
 Native mobile experience with bottom sheet
 Easier to reach buttons on mobile (bottom of screen)
 Better one-handed use
 Swipe gesture to dismiss
 Desktop keeps familiar modal experience

User Changes Applied:
- AlertDialog z-index: z-50 → z-[999] (higher than other modals)
- Dialog max-height: max-h-[100vh] → max-h-[80vh] (better desktop UX)

Files Modified:
- Payments.tsx: Responsive Dialog/Drawer implementation
- alert-dialog.tsx: Increased z-index for proper layering
This commit is contained in:
dwindown
2025-11-06 10:37:11 +07:00
parent f9161b49f4
commit cd644d339c
3 changed files with 67 additions and 11 deletions

View File

@@ -8,9 +8,11 @@ import { GenericGatewayForm } from '@/components/settings/GenericGatewayForm';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle } from 'lucide-react';
import { toast } from 'sonner';
import { useMediaQuery } from '@/hooks/use-media-query';
interface GatewayField {
id: string;
@@ -55,6 +57,7 @@ export default function PaymentsPage() {
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [togglingGateway, setTogglingGateway] = useState<string | null>(null);
const isDesktop = useMediaQuery("(min-width: 768px)");
// Fetch all payment gateways
const { data: gateways = [], isLoading, refetch } = useQuery({
@@ -261,10 +264,10 @@ export default function PaymentsPage() {
)}
</SettingsLayout>
{/* Gateway Settings Modal */}
{selectedGateway && (
{/* Gateway Settings Modal - Responsive: Dialog on desktop, Drawer on mobile */}
{selectedGateway && isDesktop && (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-2xl max-h-[100vh] flex flex-col p-0 gap-0">
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col p-0 gap-0">
<DialogHeader className="px-6 pt-6 pb-4 border-b shrink-0">
<DialogTitle>{selectedGateway.title} Settings</DialogTitle>
</DialogHeader>
@@ -277,22 +280,20 @@ export default function PaymentsPage() {
/>
</div>
{/* Footer outside scrollable area */}
<div className="border-t px-4 sm:px-6 py-3 sm:py-4 flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-0 sm:justify-between shrink-0 bg-background sm:rounded-b-lg">
<div className="border-t px-6 py-4 flex items-center justify-between shrink-0 bg-background rounded-b-lg">
<Button
type="button"
variant="outline"
onClick={() => setIsModalOpen(false)}
disabled={saveMutation.isPending}
className="order-3 sm:order-1"
>
Cancel
</Button>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 order-1 sm:order-2">
<div className="flex items-center gap-2">
<Button
type="button"
variant="ghost"
asChild
className="justify-center"
>
<a
href={selectedGateway.wc_settings_url}
@@ -310,7 +311,6 @@ export default function PaymentsPage() {
if (form) form.requestSubmit();
}}
disabled={saveMutation.isPending}
className="order-1 sm:order-2"
>
{saveMutation.isPending ? 'Saving...' : 'Save Settings'}
</Button>
@@ -319,6 +319,62 @@ export default function PaymentsPage() {
</DialogContent>
</Dialog>
)}
{selectedGateway && !isDesktop && (
<Drawer open={isModalOpen} onOpenChange={setIsModalOpen}>
<DrawerContent className="max-h-[90vh] flex flex-col">
<DrawerHeader className="border-b shrink-0">
<DrawerTitle>{selectedGateway.title} Settings</DrawerTitle>
</DrawerHeader>
<div className="flex-1 overflow-y-auto px-4 py-6 min-h-0">
<GenericGatewayForm
gateway={selectedGateway}
onSave={handleSaveGateway}
onCancel={() => setIsModalOpen(false)}
hideFooter
/>
</div>
{/* Footer outside scrollable area */}
<div className="border-t px-4 py-3 flex flex-col gap-2 shrink-0 bg-background">
<Button
onClick={() => {
const form = document.querySelector('form');
if (form) form.requestSubmit();
}}
disabled={saveMutation.isPending}
className="w-full"
>
{saveMutation.isPending ? 'Saving...' : 'Save Settings'}
</Button>
<Button
type="button"
variant="ghost"
asChild
className="w-full"
>
<a
href={selectedGateway.wc_settings_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-1"
>
View in WooCommerce
<ExternalLink className="h-4 w-4" />
</a>
</Button>
<Button
type="button"
variant="outline"
onClick={() => setIsModalOpen(false)}
disabled={saveMutation.isPending}
className="w-full"
>
Cancel
</Button>
</div>
</DrawerContent>
</Drawer>
)}
</>
);
}