feat: Initialize frontend with React, Vite, and Tailwind CSS
- Set up main entry point for React application. - Create About, Home, NotFound, Privacy, and Terms pages with SEO support. - Implement API service for file uploads and task management. - Add global styles using Tailwind CSS. - Create utility functions for SEO and text processing. - Configure Vite for development and production builds. - Set up Nginx configuration for serving frontend and backend. - Add scripts for cleanup of expired files and sitemap generation. - Implement deployment script for production environment.
This commit is contained in:
20
frontend/src/hooks/useDirection.ts
Normal file
20
frontend/src/hooks/useDirection.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* Hook that manages the HTML dir attribute based on current language.
|
||||
*/
|
||||
export function useDirection() {
|
||||
const { i18n } = useTranslation();
|
||||
const isRTL = i18n.language === 'ar';
|
||||
|
||||
useEffect(() => {
|
||||
const dir = isRTL ? 'rtl' : 'ltr';
|
||||
const lang = i18n.language;
|
||||
|
||||
document.documentElement.setAttribute('dir', dir);
|
||||
document.documentElement.setAttribute('lang', lang);
|
||||
}, [i18n.language, isRTL]);
|
||||
|
||||
return { isRTL, language: i18n.language };
|
||||
}
|
||||
110
frontend/src/hooks/useFileUpload.ts
Normal file
110
frontend/src/hooks/useFileUpload.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { uploadFile, type TaskResponse } from '@/services/api';
|
||||
|
||||
interface UseFileUploadOptions {
|
||||
endpoint: string;
|
||||
maxSizeMB?: number;
|
||||
acceptedTypes?: string[];
|
||||
extraData?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface UseFileUploadReturn {
|
||||
file: File | null;
|
||||
uploadProgress: number;
|
||||
isUploading: boolean;
|
||||
taskId: string | null;
|
||||
error: string | null;
|
||||
selectFile: (file: File) => void;
|
||||
startUpload: () => Promise<string | null>;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function useFileUpload({
|
||||
endpoint,
|
||||
maxSizeMB = 20,
|
||||
acceptedTypes,
|
||||
extraData,
|
||||
}: UseFileUploadOptions): UseFileUploadReturn {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [taskId, setTaskId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const extraDataRef = useRef(extraData);
|
||||
extraDataRef.current = extraData;
|
||||
|
||||
const selectFile = useCallback(
|
||||
(selectedFile: File) => {
|
||||
setError(null);
|
||||
setTaskId(null);
|
||||
setUploadProgress(0);
|
||||
|
||||
// Client-side size check
|
||||
const maxBytes = maxSizeMB * 1024 * 1024;
|
||||
if (selectedFile.size > maxBytes) {
|
||||
setError(`File too large. Maximum size is ${maxSizeMB}MB.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Client-side type check
|
||||
if (acceptedTypes && acceptedTypes.length > 0) {
|
||||
const ext = selectedFile.name.split('.').pop()?.toLowerCase();
|
||||
if (!ext || !acceptedTypes.includes(ext)) {
|
||||
setError(`Invalid file type. Accepted: ${acceptedTypes.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
},
|
||||
[maxSizeMB, acceptedTypes]
|
||||
);
|
||||
|
||||
const startUpload = useCallback(async (): Promise<string | null> => {
|
||||
if (!file) {
|
||||
setError('No file selected.');
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setError(null);
|
||||
setUploadProgress(0);
|
||||
|
||||
try {
|
||||
const response: TaskResponse = await uploadFile(
|
||||
endpoint,
|
||||
file,
|
||||
extraDataRef.current,
|
||||
(percent) => setUploadProgress(percent)
|
||||
);
|
||||
|
||||
setTaskId(response.task_id);
|
||||
setIsUploading(false);
|
||||
return response.task_id;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Upload failed.';
|
||||
setError(message);
|
||||
setIsUploading(false);
|
||||
return null;
|
||||
}
|
||||
}, [file, endpoint]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFile(null);
|
||||
setUploadProgress(0);
|
||||
setIsUploading(false);
|
||||
setTaskId(null);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
file,
|
||||
uploadProgress,
|
||||
isUploading,
|
||||
taskId,
|
||||
error,
|
||||
selectFile,
|
||||
startUpload,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
87
frontend/src/hooks/useTaskPolling.ts
Normal file
87
frontend/src/hooks/useTaskPolling.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { getTaskStatus, type TaskStatus, type TaskResult } from '@/services/api';
|
||||
|
||||
interface UseTaskPollingOptions {
|
||||
taskId: string | null;
|
||||
intervalMs?: number;
|
||||
onComplete?: (result: TaskResult) => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
interface UseTaskPollingReturn {
|
||||
status: TaskStatus | null;
|
||||
isPolling: boolean;
|
||||
result: TaskResult | null;
|
||||
error: string | null;
|
||||
stopPolling: () => void;
|
||||
}
|
||||
|
||||
export function useTaskPolling({
|
||||
taskId,
|
||||
intervalMs = 1500,
|
||||
onComplete,
|
||||
onError,
|
||||
}: UseTaskPollingOptions): UseTaskPollingReturn {
|
||||
const [status, setStatus] = useState<TaskStatus | null>(null);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [result, setResult] = useState<TaskResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setIsPolling(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!taskId) return;
|
||||
|
||||
setIsPolling(true);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const taskStatus = await getTaskStatus(taskId);
|
||||
setStatus(taskStatus);
|
||||
|
||||
if (taskStatus.state === 'SUCCESS') {
|
||||
stopPolling();
|
||||
const taskResult = taskStatus.result;
|
||||
|
||||
if (taskResult?.status === 'completed') {
|
||||
setResult(taskResult);
|
||||
onComplete?.(taskResult);
|
||||
} else {
|
||||
const errMsg = taskResult?.error || 'Processing failed.';
|
||||
setError(errMsg);
|
||||
onError?.(errMsg);
|
||||
}
|
||||
} else if (taskStatus.state === 'FAILURE') {
|
||||
stopPolling();
|
||||
const errMsg = taskStatus.error || 'Task failed.';
|
||||
setError(errMsg);
|
||||
onError?.(errMsg);
|
||||
}
|
||||
} catch (err) {
|
||||
stopPolling();
|
||||
const errMsg = err instanceof Error ? err.message : 'Polling failed.';
|
||||
setError(errMsg);
|
||||
onError?.(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// Poll immediately, then set interval
|
||||
poll();
|
||||
intervalRef.current = setInterval(poll, intervalMs);
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
};
|
||||
}, [taskId, intervalMs]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return { status, isPolling, result, error, stopPolling };
|
||||
}
|
||||
Reference in New Issue
Block a user