226 lines
7.6 KiB
TypeScript
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,
|
|
};
|
|
}
|