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:
@@ -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 */
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user