78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
import { useCallback, useState } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
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 createPortal(
|
||
<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-[var(--surface)] shadow-2xl overflow-hidden">
|
||
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--border)]">
|
||
<p className="text-base font-bold text-[var(--text)]">برش تصویر</p>
|
||
<button onClick={onCancel}
|
||
className="text-[var(--text-3)] hover:text-[var(--text-2)] text-xl leading-none">×</button>
|
||
</div>
|
||
|
||
<div className="relative w-full h-72 bg-[var(--surface-2)]">
|
||
<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-[var(--text-2)] 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-[var(--primary)]" />
|
||
</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-[var(--text-2)] bg-[var(--surface-2)] hover:bg-[var(--surface-3)] dark:hover:bg-[var(--surface-2)] disabled:opacity-50">
|
||
انصراف
|
||
</button>
|
||
<button onClick={handleConfirm} disabled={processing || !area}
|
||
className="px-4 py-2 rounded-xl text-sm font-medium text-[var(--on-primary)] bg-[var(--primary-600)] hover:bg-[var(--primary-700)] disabled:opacity-50">
|
||
{processing ? 'در حال برش...' : 'ذخیره'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>,
|
||
document.body,
|
||
);
|
||
}
|