Files
plagiat/src/features/modals/siModal/utils/useFileUpload.ts
2026-04-07 19:02:03 +05:00

226 lines
7.6 KiB
TypeScript

'use client';
import { useState, useCallback, useRef } from 'react';
import { UploadedFile } from './tyeps';
import { SUPPORTED_EXTENSIONS } from './wordCount';
import { useMutation } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import { toast } from 'react-toastify';
// ── API response types ────────────────────────────────────────────────────────
interface WordCountApiResponse {
word_count: number;
total_price: number;
}
interface CreateSIOrderResponse {
id: number;
order_id: number;
}
interface SIPaymentResponse {
payment_link: string;
}
// ── Return type ───────────────────────────────────────────────────────────────
interface UseFileUploadReturn {
documentName: string;
setDocumentName: (name: string) => void;
uploadedFile: UploadedFile | null;
isDragging: boolean;
isProcessing: boolean;
error: string | null;
fileInputRef: React.RefObject<HTMLInputElement | null>;
handleFileSelect: (file: File) => void;
handleDrop: (e: React.DragEvent) => void;
handleDragOver: (e: React.DragEvent) => void;
handleDragLeave: () => void;
handleRemoveFile: () => void;
openFilePicker: () => void;
handleSubmit: () => void;
canSubmit: boolean;
}
// ── Hook ─────────────────────────────────────────────────────────────────────
export function useFileUpload(): UseFileUploadReturn {
const [documentName, setDocumentName] = useState('');
const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
// ── Step 3: Payment ────────────────────────────────────────────────────────
const siPayment = useMutation({
mutationKey: ['si-payment'],
mutationFn: (order_id: number) =>
apiRequest<SIPaymentResponse>('POST', links.si_payment(order_id)),
onSuccess: (res) => {
window.open(res.data.payment_link, '_self');
},
onError: (err) => {
toast.error(
err instanceof Error ? err.message : "To'lovda xatolik yuz berdi",
);
},
});
// ── Step 2: Create SI order ────────────────────────────────────────────────
const createSIOrder = useMutation({
mutationKey: ['si-create'],
mutationFn: (data: FormData) =>
apiRequest<CreateSIOrderResponse>('POST', links.si_create, data),
onSuccess: (res) => {
siPayment.mutate(res.data.order_id);
},
onError: (err) => {
toast.error(err instanceof Error ? err.message : 'Xatolik yuz berdi');
},
});
// ── Step 1: Upload file & get word count + price ───────────────────────────
const wordCountMutation = useMutation({
mutationKey: ['si-word-count'],
mutationFn: (data: FormData) =>
apiRequest<WordCountApiResponse>('POST', links.wordCount, data),
onSuccess: (res) => {
setUploadedFile((prev) =>
prev
? {
...prev,
status: 'done',
word_count: res.data.word_count ?? 0,
total_price: res.data.total_price ?? 0,
}
: prev,
);
},
onError: (err) => {
const message = err instanceof Error ? err.message : 'Fayl yuklanmadi';
setError(message);
setUploadedFile((prev) => (prev ? { ...prev, status: 'error' } : prev));
},
});
// ── File validation ────────────────────────────────────────────────────────
const validateFile = (file: File): string | null => {
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
return `Qo'llab-quvvatlanmaydigan fayl turi. Ruxsat etilgan: ${SUPPORTED_EXTENSIONS.join(', ')}`;
}
if (file.size > 50 * 1024 * 1024) {
return 'Fayl hajmi 50 MB dan oshmasligi kerak';
}
return null;
};
// ── Step 1 trigger ─────────────────────────────────────────────────────────
const handleFileSelect = useCallback(
(file: File) => {
setError(null);
const validationError = validateFile(file);
if (validationError) {
setError(validationError);
return;
}
// Optimistic: show file chip immediately while backend responds
setUploadedFile({
file,
name: file.name,
sizeKB: Math.round(file.size / 1024),
word_count: 0,
total_price: 0,
status: 'uploading',
});
// Auto-fill document name if blank
setDocumentName((prev) =>
prev.trim() === '' ? file.name.replace(/\.[^/.]+$/, '') : prev,
);
const fd = new FormData();
fd.append('file', file);
wordCountMutation.mutate(fd);
},
[wordCountMutation],
);
// ── Step 2 trigger (Check button) ─────────────────────────────────────────
const handleSubmit = useCallback(() => {
if (!uploadedFile?.file || !documentName.trim()) return;
const fd = new FormData();
fd.append('title', documentName.trim());
fd.append('file', uploadedFile.file);
createSIOrder.mutate(fd);
}, [uploadedFile, documentName, createSIOrder]);
// ── Drag & drop ────────────────────────────────────────────────────────────
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file) handleFileSelect(file);
},
[handleFileSelect],
);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback(() => {
setIsDragging(false);
}, []);
const handleRemoveFile = useCallback(() => {
setUploadedFile(null);
setError(null);
if (fileInputRef.current) fileInputRef.current.value = '';
}, []);
const openFilePicker = useCallback(() => {
fileInputRef.current?.click();
}, []);
// ── Derived state ──────────────────────────────────────────────────────────
const isProcessing =
wordCountMutation.isPending ||
createSIOrder.isPending ||
siPayment.isPending;
const canSubmit =
documentName.trim().length > 0 &&
uploadedFile?.status === 'done' &&
!isProcessing;
return {
documentName,
setDocumentName,
uploadedFile,
isDragging,
isProcessing,
error,
fileInputRef,
handleFileSelect,
handleDrop,
handleDragOver,
handleDragLeave,
handleRemoveFile,
openFilePicker,
handleSubmit,
canSubmit,
};
}