mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
Restore drag-and-drop and add promo deactivation with Telegram popup
Payment methods drag-and-drop fixes:
- Restore original working drag-and-drop from commit 84c6d26
- Use position: relative instead of opacity for dragged items
- Remove closestCenter collision detection
- Use PointerSensor with distance: 5 without TouchSensor
- Restore touch-none and responsive padding on drag handles
- Restore original shadow and background colors
Promo deactivation feature:
- Add deactivation button to active discount banner in PromoOffersSection
- Use Telegram native popup (showPopup) in Mini App environment
- Fallback to modal for web browsers
- Add confirmation modal with warning about permanent action
- Integrate with useTelegramSDK for proper platform detection
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { promoApi, PromoOffer } from '../api/promo';
|
import { promoApi, PromoOffer } from '../api/promo';
|
||||||
import { ClockIcon, CheckIcon } from './icons';
|
import { ClockIcon, CheckIcon } from './icons';
|
||||||
|
import { useTelegramSDK } from '../hooks/useTelegramSDK';
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const formatTimeLeft = (
|
const formatTimeLeft = (
|
||||||
@@ -68,16 +70,166 @@ const getOfferDescription = (
|
|||||||
return t('promo.offers.activateDiscountHint');
|
return t('promo.offers.activateDiscountHint');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Icons for deactivation
|
||||||
|
const XCircleIcon = () => (
|
||||||
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const WarningIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-12 w-12"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------- Confirmation Modal -------
|
||||||
|
|
||||||
|
interface DeactivateConfirmModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
discountPercent: number;
|
||||||
|
isDeactivating: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeactivateConfirmModal({
|
||||||
|
isOpen,
|
||||||
|
discountPercent,
|
||||||
|
isDeactivating,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: DeactivateConfirmModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Escape key to close
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && !isDeactivating) {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [isOpen, isDeactivating, onCancel]);
|
||||||
|
|
||||||
|
// Scroll lock
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const modalContent = (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[100] flex items-center justify-center"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="deactivate-modal-title"
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
|
||||||
|
onClick={isDeactivating ? undefined : onCancel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div
|
||||||
|
className="relative mx-4 w-full max-w-sm overflow-hidden rounded-2xl border border-dark-700/50 bg-dark-900 shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Warning header */}
|
||||||
|
<div className="flex flex-col items-center bg-gradient-to-br from-warning-500/20 to-red-500/20 px-6 pb-6 pt-8">
|
||||||
|
<div className="mb-3 text-warning-400">
|
||||||
|
<WarningIcon />
|
||||||
|
</div>
|
||||||
|
<h2 id="deactivate-modal-title" className="text-center text-lg font-bold text-dark-100">
|
||||||
|
{t('promo.deactivate.confirmTitle')}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-sm text-dark-300">
|
||||||
|
{t('promo.deactivate.confirmDescription', { percent: discountPercent })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning text */}
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className="rounded-lg border border-warning-500/20 bg-warning-500/10 px-4 py-3">
|
||||||
|
<p className="text-center text-sm text-warning-400">{t('promo.deactivate.warning')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-3 px-6 pb-6">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isDeactivating}
|
||||||
|
className="flex-1 rounded-xl bg-dark-800 py-3 font-semibold text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={isDeactivating}
|
||||||
|
className="flex-1 rounded-xl bg-gradient-to-r from-red-500 to-red-600 py-3 font-semibold text-white shadow-lg shadow-red-500/25 transition-all hover:from-red-400 hover:to-red-500 active:from-red-600 active:to-red-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isDeactivating ? (
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||||
|
{t('promo.deactivate.deactivating')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
t('promo.deactivate.confirm')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
return createPortal(modalContent, document.body);
|
||||||
|
}
|
||||||
|
return modalContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------- Main Component -------
|
||||||
|
|
||||||
interface PromoOffersSectionProps {
|
interface PromoOffersSectionProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PromoOffersSection({ className = '' }: PromoOffersSectionProps) {
|
export default function PromoOffersSection({ className = '' }: PromoOffersSectionProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { isTelegramWebApp } = useTelegramSDK();
|
||||||
const [claimingId, setClaimingId] = useState<number | null>(null);
|
const [claimingId, setClaimingId] = useState<number | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||||
|
|
||||||
// Fetch available offers
|
// Fetch available offers
|
||||||
const { data: offers = [], isLoading: offersLoading } = useQuery({
|
const { data: offers = [], isLoading: offersLoading } = useQuery({
|
||||||
@@ -112,6 +264,26 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Deactivate discount mutation
|
||||||
|
const deactivateMutation = useMutation({
|
||||||
|
mutationFn: promoApi.deactivateDiscount,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['active-discount'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['promo-offers'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setSuccessMessage(t('promo.deactivate.success'));
|
||||||
|
setTimeout(() => setSuccessMessage(null), 5000);
|
||||||
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
const axiosErr = error as { response?: { data?: { detail?: string } } };
|
||||||
|
setErrorMessage(axiosErr.response?.data?.detail || t('promo.deactivate.error'));
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setTimeout(() => setErrorMessage(null), 5000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleClaim = (offerId: number) => {
|
const handleClaim = (offerId: number) => {
|
||||||
setClaimingId(offerId);
|
setClaimingId(offerId);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
@@ -119,6 +291,49 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
claimMutation.mutate(offerId);
|
claimMutation.mutate(offerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUseNow = () => {
|
||||||
|
navigate('/subscription', { state: { scrollToExtend: true } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeactivateClick = () => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
setSuccessMessage(null);
|
||||||
|
|
||||||
|
// Use Telegram native popup in Mini App
|
||||||
|
if (isTelegramWebApp && window.Telegram?.WebApp?.showPopup) {
|
||||||
|
window.Telegram.WebApp.showPopup(
|
||||||
|
{
|
||||||
|
title: t('promo.deactivate.confirmTitle'),
|
||||||
|
message: t('promo.deactivate.confirmDescription', {
|
||||||
|
percent: activeDiscount?.discount_percent || 0,
|
||||||
|
}),
|
||||||
|
buttons: [
|
||||||
|
{ id: 'cancel', type: 'cancel' },
|
||||||
|
{ id: 'confirm', type: 'destructive', text: t('promo.deactivate.confirm') },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(button_id: string) => {
|
||||||
|
if (button_id === 'confirm') {
|
||||||
|
deactivateMutation.mutate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback to modal for web
|
||||||
|
setShowConfirmModal(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDeactivate = () => {
|
||||||
|
deactivateMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseConfirmModal = () => {
|
||||||
|
if (!deactivateMutation.isPending) {
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Filter unclaimed and active offers
|
// Filter unclaimed and active offers
|
||||||
const availableOffers = offers.filter((o) => o.is_active && !o.is_claimed);
|
const availableOffers = offers.filter((o) => o.is_active && !o.is_claimed);
|
||||||
|
|
||||||
@@ -133,55 +348,59 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-4 ${className}`}>
|
<div className={`space-y-4 ${className}`}>
|
||||||
{/* Active Discount Banner - clickable, navigates to subscription */}
|
{/* Active Discount Banner with actions */}
|
||||||
{activeDiscount && activeDiscount.is_active && activeDiscount.discount_percent > 0 && (
|
{activeDiscount && activeDiscount.is_active && activeDiscount.discount_percent > 0 && (
|
||||||
<Link
|
<div className="card border-success-500/30 bg-gradient-to-br from-success-500/10 to-accent-500/5">
|
||||||
to="/subscription"
|
<div className="flex flex-col gap-4">
|
||||||
state={{ scrollToExtend: true }}
|
{/* Header */}
|
||||||
className="card group block cursor-pointer border-success-500/30 bg-gradient-to-br from-success-500/10 to-accent-500/5 transition-all duration-200 hover:scale-[1.01] hover:border-success-500/50 hover:from-success-500/20 hover:to-accent-500/10 hover:shadow-lg hover:shadow-success-500/10 active:scale-[0.99]"
|
<div className="flex items-center gap-4">
|
||||||
>
|
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl bg-success-500/20 text-success-400">
|
||||||
<div className="flex items-center gap-4">
|
<span className="text-2xl">🏷️</span>
|
||||||
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl bg-success-500/20 text-success-400 transition-transform duration-200 group-hover:scale-110">
|
|
||||||
<span className="text-2xl">🏷️</span>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="mb-1 flex items-center gap-2">
|
|
||||||
<h3 className="font-semibold text-dark-100">
|
|
||||||
{t('promo.offers.discountActiveTitle', {
|
|
||||||
percent: activeDiscount.discount_percent,
|
|
||||||
})}
|
|
||||||
</h3>
|
|
||||||
<span className="rounded bg-success-500/20 px-2 py-0.5 text-xs font-bold text-success-400">
|
|
||||||
-{activeDiscount.discount_percent}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-sm text-dark-400">
|
<div className="min-w-0 flex-1">
|
||||||
{activeDiscount.expires_at && (
|
<div className="mb-1 flex flex-wrap items-center gap-2">
|
||||||
<div className="flex items-center gap-1">
|
<h3 className="font-semibold text-dark-100">
|
||||||
<ClockIcon />
|
{t('promo.offers.discountActiveTitle', {
|
||||||
<span>
|
percent: activeDiscount.discount_percent,
|
||||||
{t('promo.offers.expires', {
|
})}
|
||||||
time: formatTimeLeft(activeDiscount.expires_at, t),
|
</h3>
|
||||||
})}
|
<span className="rounded bg-success-500/20 px-2 py-0.5 text-xs font-bold text-success-400">
|
||||||
</span>
|
-{activeDiscount.discount_percent}%
|
||||||
</div>
|
</span>
|
||||||
)}
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-dark-400">
|
||||||
|
{activeDiscount.expires_at && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ClockIcon />
|
||||||
|
<span>
|
||||||
|
{t('promo.offers.expires', {
|
||||||
|
time: formatTimeLeft(activeDiscount.expires_at, t),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Arrow indicator */}
|
|
||||||
<div className="flex-shrink-0 text-dark-500 transition-all duration-200 group-hover:translate-x-1 group-hover:text-success-400">
|
{/* Action Buttons */}
|
||||||
<svg
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
className="h-5 w-5"
|
<button
|
||||||
fill="none"
|
onClick={handleUseNow}
|
||||||
viewBox="0 0 24 24"
|
className="flex-1 rounded-xl bg-gradient-to-r from-success-500 to-success-600 px-4 py-2.5 font-semibold text-white shadow-lg shadow-success-500/25 transition-all hover:from-success-400 hover:to-success-500 active:from-success-600 active:to-success-700"
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
{t('promo.useNow')}
|
||||||
</svg>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeactivateClick}
|
||||||
|
className="flex items-center justify-center gap-1.5 rounded-xl border border-dark-600/50 bg-dark-900/50 px-4 py-2.5 text-sm text-dark-400 transition-colors hover:border-red-500/30 hover:bg-red-500/10 hover:text-red-400"
|
||||||
|
>
|
||||||
|
<XCircleIcon />
|
||||||
|
<span>{t('promo.deactivate.button')}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Success/Error Messages */}
|
{/* Success/Error Messages */}
|
||||||
@@ -266,6 +485,17 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Deactivation Confirmation Modal */}
|
||||||
|
{activeDiscount && (
|
||||||
|
<DeactivateConfirmModal
|
||||||
|
isOpen={showConfirmModal}
|
||||||
|
discountPercent={activeDiscount.discount_percent || 0}
|
||||||
|
isDeactivating={deactivateMutation.isPending}
|
||||||
|
onConfirm={handleConfirmDeactivate}
|
||||||
|
onCancel={handleCloseConfirmModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
PointerSensor,
|
PointerSensor,
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
@@ -101,11 +99,11 @@ function SortablePaymentCard({ config, onClick }: SortableCardProps) {
|
|||||||
id: config.method_id,
|
id: config.method_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = {
|
const style: React.CSSProperties = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? 50 : undefined,
|
zIndex: isDragging ? 50 : undefined,
|
||||||
opacity: isDragging ? 0.85 : 1,
|
position: isDragging ? 'relative' : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayName = config.display_name || config.default_display_name;
|
const displayName = config.display_name || config.default_display_name;
|
||||||
@@ -140,19 +138,20 @@ function SortablePaymentCard({ config, onClick }: SortableCardProps) {
|
|||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className={`group flex items-center gap-3 rounded-xl border p-4 transition-all ${
|
className={`group flex items-center gap-3 rounded-xl border p-4 ${
|
||||||
isDragging
|
isDragging
|
||||||
? 'border-accent-500/50 bg-dark-700/80 shadow-xl shadow-accent-500/10'
|
? 'border-accent-500/50 bg-dark-800 shadow-xl shadow-accent-500/20'
|
||||||
: config.is_enabled
|
: config.is_enabled
|
||||||
? 'border-dark-700/50 bg-dark-800/50 hover:border-dark-600'
|
? 'border-dark-700/50 bg-dark-800/50 hover:border-dark-600'
|
||||||
: 'border-dark-800/50 bg-dark-900/30 opacity-60'
|
: 'border-dark-800/50 bg-dark-900/30 opacity-60'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Drag handle */}
|
{/* Drag handle */}
|
||||||
|
{/* Drag handle - larger touch target for mobile */}
|
||||||
<button
|
<button
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
className="flex-shrink-0 cursor-grab touch-manipulation rounded-lg p-1.5 text-dark-500 hover:bg-dark-700/50 hover:text-dark-300 active:cursor-grabbing"
|
className="flex-shrink-0 cursor-grab touch-none rounded-lg p-2.5 text-dark-500 hover:bg-dark-700/50 hover:text-dark-300 active:cursor-grabbing sm:p-1.5"
|
||||||
title={t('admin.paymentMethods.dragToReorder')}
|
title={t('admin.paymentMethods.dragToReorder')}
|
||||||
>
|
>
|
||||||
<GripIcon />
|
<GripIcon />
|
||||||
@@ -267,10 +266,9 @@ export default function AdminPaymentMethods() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// DnD sensors
|
// DnD sensors - PointerSensor handles both mouse and touch
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }),
|
|
||||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -362,7 +360,6 @@ export default function AdminPaymentMethods() {
|
|||||||
) : methods.length > 0 ? (
|
) : methods.length > 0 ? (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
onDragCancel={handleDragCancel}
|
onDragCancel={handleDragCancel}
|
||||||
|
|||||||
Reference in New Issue
Block a user