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

@@ -57,7 +57,7 @@ function useFullscreen() {
.wnw-fullscreen .woonoow-fullscreen-root { .wnw-fullscreen .woonoow-fullscreen-root {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 9999; z-index: 999;
background: var(--background, #fff); background: var(--background, #fff);
height: 100dvh; /* ensure full viewport height on mobile/desktop */ height: 100dvh; /* ensure full viewport height on mobile/desktop */
overflow: hidden; /* prevent double scrollbars; inner <main> handles scrolling */ overflow: hidden; /* prevent double scrollbars; inner <main> handles scrolling */

View File

@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-[999] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className
)} )}
{...props} {...props}
@@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-[999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}

View File

@@ -8,9 +8,11 @@ import { GenericGatewayForm } from '@/components/settings/GenericGatewayForm';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; 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 { Alert, AlertDescription } from '@/components/ui/alert';
import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle } from 'lucide-react'; import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useMediaQuery } from '@/hooks/use-media-query';
interface GatewayField { interface GatewayField {
id: string; id: string;
@@ -55,6 +57,7 @@ export default function PaymentsPage() {
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway | null>(null); const [selectedGateway, setSelectedGateway] = useState<PaymentGateway | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [togglingGateway, setTogglingGateway] = useState<string | null>(null); const [togglingGateway, setTogglingGateway] = useState<string | null>(null);
const isDesktop = useMediaQuery("(min-width: 768px)");
// Fetch all payment gateways // Fetch all payment gateways
const { data: gateways = [], isLoading, refetch } = useQuery({ const { data: gateways = [], isLoading, refetch } = useQuery({
@@ -261,10 +264,10 @@ export default function PaymentsPage() {
)} )}
</SettingsLayout> </SettingsLayout>
{/* Gateway Settings Modal */} {/* Gateway Settings Modal - Responsive: Dialog on desktop, Drawer on mobile */}
{selectedGateway && ( {selectedGateway && isDesktop && (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}> <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"> <DialogHeader className="px-6 pt-6 pb-4 border-b shrink-0">
<DialogTitle>{selectedGateway.title} Settings</DialogTitle> <DialogTitle>{selectedGateway.title} Settings</DialogTitle>
</DialogHeader> </DialogHeader>
@@ -277,22 +280,20 @@ export default function PaymentsPage() {
/> />
</div> </div>
{/* Footer outside scrollable area */} {/* 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 <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={() => setIsModalOpen(false)} onClick={() => setIsModalOpen(false)}
disabled={saveMutation.isPending} disabled={saveMutation.isPending}
className="order-3 sm:order-1"
> >
Cancel Cancel
</Button> </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 <Button
type="button" type="button"
variant="ghost" variant="ghost"
asChild asChild
className="justify-center"
> >
<a <a
href={selectedGateway.wc_settings_url} href={selectedGateway.wc_settings_url}
@@ -310,7 +311,6 @@ export default function PaymentsPage() {
if (form) form.requestSubmit(); if (form) form.requestSubmit();
}} }}
disabled={saveMutation.isPending} disabled={saveMutation.isPending}
className="order-1 sm:order-2"
> >
{saveMutation.isPending ? 'Saving...' : 'Save Settings'} {saveMutation.isPending ? 'Saving...' : 'Save Settings'}
</Button> </Button>
@@ -319,6 +319,62 @@ export default function PaymentsPage() {
</DialogContent> </DialogContent>
</Dialog> </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>
)}
</> </>
); );
} }