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 {
position: fixed;
inset: 0;
z-index: 9999;
z-index: 999;
background: var(--background, #fff);
height: 100dvh; /* ensure full viewport height on mobile/desktop */
overflow: hidden; /* prevent double scrollbars; inner <main> handles scrolling */

View File

@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
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
)}
{...props}
@@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
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
)}
{...props}

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>
)}
</>
);
}