openreader/src/components/DocumentUploader.tsx
Richard Roberson b576910523 refactor(client): centralize client-side API calls and refine types
Abstracted direct fetch calls across components and contexts into new functions within `src/lib/client.ts`. This provides a consistent and centralized interface for interacting with backend APIs.

- Introduced `src/lib/client.ts` to encapsulate API request logic.
- Standardized audio buffer types (`TTSAudioBuffer`, `TTSAudioBytes`) in `src/types/tts.ts`.
- Moved client-specific request types (`TTSRequestPayload`, `TTSRequestHeaders`, `TTSRetryOptions`) to `src/types/client.ts`.
- Updated API routes and consumer components/contexts to leverage the new client library functions and type definitions.
- Removed `src/utils/audio.ts` as its utility functions are now part of `src/lib/client.ts`.
2025-11-21 23:33:41 -07:00

127 lines
4.9 KiB
TypeScript

'use client';
import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { UploadIcon } from '@/components/icons/Icons';
import { useDocuments } from '@/contexts/DocumentContext';
import { convertDocxToPdf as convertDocxToPdfClient } from '@/lib/client';
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
interface DocumentUploaderProps {
className?: string;
variant?: 'default' | 'compact';
}
export function DocumentUploader({ className = '', variant = 'default' }: DocumentUploaderProps) {
const {
addPDFDocument: addPDF,
addEPUBDocument: addEPUB,
addHTMLDocument: addHTML
} = useDocuments();
const [isUploading, setIsUploading] = useState(false);
const [isConverting, setIsConverting] = useState(false);
const [error, setError] = useState<string | null>(null);
const convertDocxToPdf = async (file: File): Promise<File> => {
const pdfBlob = await convertDocxToPdfClient(file);
return new File([pdfBlob], file.name.replace(/\.docx$/, '.pdf'), {
type: 'application/pdf',
});
};
const onDrop = useCallback(async (acceptedFiles: File[]) => {
if (!acceptedFiles || acceptedFiles.length === 0) return;
setIsUploading(true);
setError(null);
try {
for (const file of acceptedFiles) {
if (file.type === 'application/pdf') {
await addPDF(file);
} else if (file.type === 'application/epub+zip') {
await addEPUB(file);
} else if (file.type === 'text/plain' || file.type === 'text/markdown' || file.name.endsWith('.md')) {
await addHTML(file);
} else if (isDev && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
setIsUploading(false);
setIsConverting(true);
const pdfFile = await convertDocxToPdf(file);
await addPDF(pdfFile);
setIsConverting(false);
setIsUploading(true);
}
}
} catch (err) {
setError('Failed to upload file. Please try again.');
console.error('Upload error:', err);
} finally {
setIsUploading(false);
setIsConverting(false);
}
}, [addHTML, addPDF, addEPUB]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'application/pdf': ['.pdf'],
'application/epub+zip': ['.epub'],
'text/plain': ['.txt'],
'text/markdown': ['.md'],
...(isDev ? {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx']
} : {})
},
multiple: true,
disabled: isUploading || isConverting
});
const containerBase = `w-full border-2 border-dashed rounded-lg ${isDragActive ? 'border-accent bg-base' : 'border-muted'} transform transition-transform duration-200 ease-in-out ${(isUploading || isConverting) ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:border-accent hover:bg-base hover:scale-[1.008]'} ${className}`;
const paddingClass = variant === 'compact' ? 'py-1.5 px-2' : 'py-5 px-3';
return (
<div
{...getRootProps()}
className={`${containerBase} ${paddingClass}`}
>
<input {...getInputProps()} />
{variant === 'compact' ? (
<div className="flex items-center gap-2 text-left">
<UploadIcon className="w-5 h-5 text-muted" />
{isUploading ? (
<p className="text-xs font-medium text-foreground">Uploading</p>
) : isConverting ? (
<p className="text-xs font-medium text-foreground">Converting DOCX</p>
) : (
<div className="flex items-center gap-2">
<p className="text-xs font-medium text-foreground">
{isDragActive ? 'Drop files here' : 'Drop files or click'}
</p>
{error && <p className="text-xs text-red-500">{error}</p>}
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center text-center">
<UploadIcon className="w-7 h-7 sm:w-10 sm:h-10 mb-2 text-muted" />
{isUploading ? (
<p className="text-sm sm:text-lg font-semibold text-foreground">Uploading file...</p>
) : isConverting ? (
<p className="text-sm sm:text-lg font-semibold text-foreground">Converting DOCX to PDF...</p>
) : (
<>
<p className="mb-2 text-sm sm:text-lg font-semibold text-foreground">
{isDragActive ? 'Drop your file(s) here' : 'Drop your file(s) here, or click to select'}
</p>
<p className="text-xs sm:text-sm text-muted">
{isDev ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
</p>
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
</>
)}
</div>
)}
</div>
);
}