Files
clinicpro/assets/admin/components/ImageCropModal.tsx
T
hamed 6ade17a5b4 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.
2026-07-04 21:50:55 +03:30

76 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useState } from 'react';
import Cropper, { type Area } from 'react-easy-crop';
import { getCroppedImage } from '../lib/cropImage';
interface ImageCropModalProps {
src: string;
fileName: string;
onCancel: () => void;
onConfirm: (file: File) => void;
}
export default function ImageCropModal({ src, fileName, onCancel, onConfirm }: ImageCropModalProps) {
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [area, setArea] = useState<Area | null>(null);
const [processing, setProcessing] = useState(false);
const onCropComplete = useCallback((_: Area, pixels: Area) => setArea(pixels), []);
const handleConfirm = async () => {
if (!area) return;
setProcessing(true);
try {
const file = await getCroppedImage(src, area, fileName);
onConfirm(file);
} finally {
setProcessing(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" dir="rtl">
<div className="w-full max-w-md rounded-2xl bg-white dark:bg-gray-900 shadow-2xl overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100 dark:border-gray-800">
<p className="text-base font-bold text-slate-800 dark:text-slate-100">برش تصویر</p>
<button onClick={onCancel}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-xl leading-none">×</button>
</div>
<div className="relative w-full h-72 bg-slate-100 dark:bg-gray-800">
<Cropper
image={src}
crop={crop}
zoom={zoom}
aspect={1}
cropShape="round"
showGrid={false}
onCropChange={setCrop}
onZoomChange={setZoom}
onCropComplete={onCropComplete}
/>
</div>
<div className="px-5 py-4 space-y-4">
<div className="flex items-center gap-3">
<span className="text-xs text-slate-500 dark:text-slate-400 shrink-0">بزرگ‌نمایی</span>
<input type="range" min={1} max={3} step={0.01} value={zoom}
onChange={e => setZoom(Number(e.target.value))}
className="w-full accent-indigo-600" />
</div>
<div className="flex items-center justify-end gap-3">
<button onClick={onCancel} disabled={processing}
className="px-4 py-2 rounded-xl text-sm font-medium text-slate-600 dark:text-slate-300 bg-slate-100 dark:bg-gray-800 hover:bg-slate-200 dark:hover:bg-gray-700 disabled:opacity-50">
انصراف
</button>
<button onClick={handleConfirm} disabled={processing || !area}
className="px-4 py-2 rounded-xl text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50">
{processing ? 'در حال برش...' : 'ذخیره'}
</button>
</div>
</div>
</div>
</div>
);
}