feat: add HeroUploadZone component for file uploads and ToolSelectorModal for tool selection
- Implemented HeroUploadZone for drag-and-drop file uploads with support for various file types (PDF, images, video, Word documents). - Integrated ToolSelectorModal to display available tools based on the uploaded file type. - Added Zustand store for managing file state across routes. - Updated multiple tool components to accept files from the new upload zone. - Enhanced internationalization support with new translations for upload prompts and tool selection. - Styled the upload zone and modal for improved user experience.
This commit is contained in:
149
frontend/src/components/shared/HeroUploadZone.tsx
Normal file
149
frontend/src/components/shared/HeroUploadZone.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Upload, Sparkles } from 'lucide-react';
|
||||
import ToolSelectorModal from '@/components/shared/ToolSelectorModal';
|
||||
import { getToolsForFile, detectFileCategory, getCategoryLabel } from '@/utils/fileRouting';
|
||||
import type { ToolOption } from '@/utils/fileRouting';
|
||||
|
||||
/**
|
||||
* The MIME types we accept on the homepage smart upload zone.
|
||||
* Covers PDF, images, video, and Word documents.
|
||||
*/
|
||||
const ACCEPTED_TYPES = {
|
||||
'application/pdf': ['.pdf'],
|
||||
'image/jpeg': ['.jpg', '.jpeg'],
|
||||
'image/png': ['.png'],
|
||||
'image/webp': ['.webp'],
|
||||
'video/mp4': ['.mp4'],
|
||||
'video/webm': ['.webm'],
|
||||
'application/msword': ['.doc'],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
||||
};
|
||||
|
||||
export default function HeroUploadZone() {
|
||||
const { t } = useTranslation();
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [matchedTools, setMatchedTools] = useState<ToolOption[]>([]);
|
||||
const [fileTypeLabel, setFileTypeLabel] = useState('');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
setError(null);
|
||||
|
||||
if (acceptedFiles.length === 0) return;
|
||||
|
||||
const file = acceptedFiles[0];
|
||||
const tools = getToolsForFile(file);
|
||||
|
||||
if (tools.length === 0) {
|
||||
setError(t('home.unsupportedFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
const category = detectFileCategory(file);
|
||||
const label = getCategoryLabel(category);
|
||||
|
||||
setSelectedFile(file);
|
||||
setMatchedTools(tools);
|
||||
setFileTypeLabel(label);
|
||||
setModalOpen(true);
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: ACCEPTED_TYPES,
|
||||
maxFiles: 1,
|
||||
maxSize: 100 * 1024 * 1024, // 100 MB (matches nginx config)
|
||||
onDropRejected: (rejections) => {
|
||||
const rejection = rejections[0];
|
||||
if (rejection?.errors[0]?.code === 'file-too-large') {
|
||||
setError(t('common.maxSize', { size: 100 }));
|
||||
} else {
|
||||
setError(t('home.unsupportedFile'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setModalOpen(false);
|
||||
setSelectedFile(null);
|
||||
setMatchedTools([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-8 max-w-2xl">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`hero-upload-zone ${isDragActive ? 'drag-active' : ''}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`mb-4 flex h-16 w-16 items-center justify-center rounded-2xl transition-colors ${
|
||||
isDragActive
|
||||
? 'bg-primary-100 dark:bg-primary-900/30'
|
||||
: 'bg-primary-50 dark:bg-primary-900/20'
|
||||
}`}
|
||||
>
|
||||
<Upload
|
||||
className={`h-8 w-8 transition-colors ${
|
||||
isDragActive
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-primary-500 dark:text-primary-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CTA Text */}
|
||||
<p className="mb-1 text-lg font-semibold text-slate-800 dark:text-slate-200">
|
||||
{t('home.uploadCta')}
|
||||
</p>
|
||||
<p className="mb-3 text-sm text-slate-500 dark:text-slate-400">
|
||||
{t('home.uploadOr')}
|
||||
</p>
|
||||
|
||||
{/* Supported formats */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
{['PDF', 'Word', 'JPG', 'PNG', 'WebP', 'MP4'].map((format) => (
|
||||
<span
|
||||
key={format}
|
||||
className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600 dark:bg-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{format}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* File size hint */}
|
||||
<p className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-400 dark:text-slate-500">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{t('home.uploadSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mt-3 rounded-xl bg-red-50 p-3 ring-1 ring-red-200 dark:bg-red-900/20 dark:ring-red-800">
|
||||
<p className="text-center text-sm text-red-700 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool Selector Modal */}
|
||||
<ToolSelectorModal
|
||||
isOpen={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
file={selectedFile}
|
||||
tools={matchedTools}
|
||||
fileTypeLabel={fileTypeLabel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
150
frontend/src/components/shared/ToolSelectorModal.tsx
Normal file
150
frontend/src/components/shared/ToolSelectorModal.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, File as FileIcon } from 'lucide-react';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
import { formatFileSize } from '@/utils/textTools';
|
||||
import type { ToolOption } from '@/utils/fileRouting';
|
||||
|
||||
interface ToolSelectorModalProps {
|
||||
/** Whether the modal is open */
|
||||
isOpen: boolean;
|
||||
/** Callback to close the modal */
|
||||
onClose: () => void;
|
||||
/** The uploaded file */
|
||||
file: File | null;
|
||||
/** Available tools for this file type */
|
||||
tools: ToolOption[];
|
||||
/** Detected file type label (e.g. "PDF", "Image") */
|
||||
fileTypeLabel: string;
|
||||
}
|
||||
|
||||
export default function ToolSelectorModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
file,
|
||||
tools,
|
||||
fileTypeLabel,
|
||||
}: ToolSelectorModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const setStoreFile = useFileStore((s) => s.setFile);
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Prevent body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleToolSelect = useCallback(
|
||||
(tool: ToolOption) => {
|
||||
if (!file) return;
|
||||
|
||||
// Store file in zustand for the target tool to pick up
|
||||
setStoreFile(file);
|
||||
|
||||
// Navigate to the tool page
|
||||
navigate(tool.path);
|
||||
onClose();
|
||||
},
|
||||
[file, setStoreFile, navigate, onClose]
|
||||
);
|
||||
|
||||
// Close on backdrop click
|
||||
const handleBackdropClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
if (!isOpen || !file) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"
|
||||
onClick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="tool-selector-title"
|
||||
>
|
||||
<div className="modal-content w-full max-w-lg rounded-2xl bg-white p-6 shadow-2xl ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
||||
{/* Header */}
|
||||
<div className="mb-5 flex items-start justify-between">
|
||||
<div>
|
||||
<h2
|
||||
id="tool-selector-title"
|
||||
className="text-lg font-bold text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
{t('home.selectTool')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
{t('home.fileDetected', { type: fileTypeLabel })}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg p-1.5 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600 dark:hover:bg-slate-700 dark:hover:text-slate-300"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* File Info */}
|
||||
<div className="mb-5 flex items-center gap-3 rounded-xl bg-primary-50 p-3 ring-1 ring-primary-200 dark:bg-primary-900/20 dark:ring-primary-800">
|
||||
<FileIcon className="h-8 w-8 flex-shrink-0 text-primary-600 dark:text-primary-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{formatFileSize(file.size)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{tools.map((tool) => {
|
||||
const Icon = tool.icon;
|
||||
return (
|
||||
<button
|
||||
key={tool.key}
|
||||
onClick={() => handleToolSelect(tool)}
|
||||
className="group flex flex-col items-center gap-2 rounded-xl p-4 ring-1 ring-slate-200 transition-all hover:ring-primary-300 hover:shadow-md dark:ring-slate-700 dark:hover:ring-primary-600"
|
||||
>
|
||||
<div
|
||||
className={`flex h-10 w-10 items-center justify-center rounded-xl ${tool.bgColor}`}
|
||||
>
|
||||
<Icon className={`h-5 w-5 ${tool.iconColor}`} />
|
||||
</div>
|
||||
<span className="text-center text-xs font-medium text-slate-700 group-hover:text-primary-600 dark:text-slate-300 dark:group-hover:text-primary-400">
|
||||
{t(`tools.${tool.key}.shortDesc`)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { ListOrdered } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
type Position = 'bottom-center' | 'bottom-right' | 'bottom-left' | 'top-center' | 'top-right' | 'top-left';
|
||||
|
||||
@@ -40,6 +41,16 @@ export default function AddPageNumbers() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { ImageIcon } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
type OutputFormat = 'jpg' | 'png' | 'webp';
|
||||
|
||||
@@ -40,6 +41,16 @@ export default function ImageConverter() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { FileImage } from 'lucide-react';
|
||||
@@ -7,6 +7,7 @@ import ProgressBar from '@/components/shared/ProgressBar';
|
||||
import DownloadButton from '@/components/shared/DownloadButton';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function ImagesToPdf() {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,6 +23,16 @@ export default function ImagesToPdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
setFiles((prev) => [...prev, storeFile]);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const acceptedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'];
|
||||
|
||||
const handleFilesSelect = (newFiles: FileList | File[]) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Layers } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { uploadFile, type TaskResponse } from '@/services/api';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function MergePdf() {
|
||||
const { t } = useTranslation();
|
||||
@@ -25,6 +26,16 @@ export default function MergePdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
setFiles((prev) => [...prev, storeFile]);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleFilesSelect = (newFiles: FileList | File[]) => {
|
||||
const fileArray = Array.from(newFiles).filter(
|
||||
(f) => f.type === 'application/pdf'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Minimize2 } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
type Quality = 'low' | 'medium' | 'high';
|
||||
|
||||
@@ -39,6 +40,16 @@ export default function PdfCompressor() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { ImageIcon } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
type OutputFormat = 'png' | 'jpg';
|
||||
|
||||
@@ -40,6 +41,16 @@ export default function PdfToImages() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { FileText } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function PdfToWord() {
|
||||
const { t } = useTranslation();
|
||||
@@ -35,6 +36,16 @@ export default function PdfToWord() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Lock } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function ProtectPdf() {
|
||||
const { t } = useTranslation();
|
||||
@@ -40,6 +41,16 @@ export default function ProtectPdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!passwordsMatch) return;
|
||||
const id = await startUpload();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { RotateCw } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
type Rotation = 90 | 180 | 270;
|
||||
|
||||
@@ -39,6 +40,16 @@ export default function RotatePdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Scissors } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
type SplitMode = 'all' | 'range';
|
||||
|
||||
@@ -40,6 +41,16 @@ export default function SplitPdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (mode === 'range' && !pages.trim()) return;
|
||||
const id = await startUpload();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Unlock } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function UnlockPdf() {
|
||||
const { t } = useTranslation();
|
||||
@@ -37,6 +38,16 @@ export default function UnlockPdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!password) return;
|
||||
const id = await startUpload();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Film } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function VideoToGif() {
|
||||
const { t } = useTranslation();
|
||||
@@ -45,6 +46,16 @@ export default function VideoToGif() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Droplets } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function WatermarkPdf() {
|
||||
const { t } = useTranslation();
|
||||
@@ -38,6 +39,16 @@ export default function WatermarkPdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { FileOutput } from 'lucide-react';
|
||||
@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
|
||||
import { useFileUpload } from '@/hooks/useFileUpload';
|
||||
import { useTaskPolling } from '@/hooks/useTaskPolling';
|
||||
import { generateToolSchema } from '@/utils/seo';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
|
||||
export default function WordToPdf() {
|
||||
const { t } = useTranslation();
|
||||
@@ -35,6 +36,16 @@ export default function WordToPdf() {
|
||||
onError: () => setPhase('done'),
|
||||
});
|
||||
|
||||
// Accept file from homepage smart upload
|
||||
const storeFile = useFileStore((s) => s.file);
|
||||
const clearStoreFile = useFileStore((s) => s.clearFile);
|
||||
useEffect(() => {
|
||||
if (storeFile) {
|
||||
selectFile(storeFile);
|
||||
clearStoreFile();
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleUpload = async () => {
|
||||
const id = await startUpload();
|
||||
if (id) setPhase('processing');
|
||||
|
||||
Reference in New Issue
Block a user