feat: implement OTP sending via Kavenegar VerifyLookup and add image cropping modal
- Added support for sending OTP messages using Kavenegar's VerifyLookup method, ensuring compliance with specified token formatting and template usage. - Updated OtpService to handle new template parameters and fallback mechanisms. - Introduced ImageCropModal component for cropping images with a user-friendly interface. - Created utility function for cropping images and generating downloadable files.
This commit is contained in:
@@ -29,6 +29,7 @@ import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
import NotificationMobileCard from '../components/ui/NotificationMobileCard';
|
||||
import GlobalSearchableSelect from '../components/ui/SearchableSelect';
|
||||
import PersianDatePicker from '../components/ui/PersianDatePicker';
|
||||
import ImageCropModal from '../components/ImageCropModal';
|
||||
|
||||
// Fix leaflet default marker icons
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
@@ -2335,6 +2336,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
const [toggleConfirm, setToggleConfirm] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [uploadingImg, setUploadingImg] = useState(false);
|
||||
const [cropState, setCropState] = useState<{ src: string; name: string } | null>(null);
|
||||
const [addrModalOpen, setAddrModalOpen] = useState(false);
|
||||
const [editingAddr, setEditingAddr] = useState<AddressData | null>(null);
|
||||
const [deletingAddrId, setDeletingAddrId] = useState<string | null>(null);
|
||||
@@ -2475,6 +2477,17 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
|
||||
// ── Image upload ──
|
||||
|
||||
const openCrop = useCallback((file: File) => {
|
||||
setCropState({ src: URL.createObjectURL(file), name: file.name });
|
||||
}, []);
|
||||
|
||||
const closeCrop = useCallback(() => {
|
||||
setCropState(prev => {
|
||||
if (prev) URL.revokeObjectURL(prev.src);
|
||||
return null;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleImageUpload = useCallback(async (file: File) => {
|
||||
setUploadingImg(true);
|
||||
try {
|
||||
@@ -2567,7 +2580,15 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<DoctorAvatar name={doctor.name} img={mainImage} idx={idNum}
|
||||
onUpload={primaryRole !== 'clinic' && !isReadOnly ? handleImageUpload : undefined} uploading={uploadingImg} />
|
||||
onUpload={primaryRole !== 'clinic' && !isReadOnly ? openCrop : undefined} uploading={uploadingImg} />
|
||||
{cropState && (
|
||||
<ImageCropModal
|
||||
src={cropState.src}
|
||||
fileName={cropState.name}
|
||||
onCancel={closeCrop}
|
||||
onConfirm={(file) => { closeCrop(); handleImageUpload(file); }}
|
||||
/>
|
||||
)}
|
||||
{primaryRole !== 'clinic' && !isReadOnly && (
|
||||
<span className="text-[10px] text-slate-400 dark:text-slate-500">کلیک برای تغییر عکس</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user