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:
Your Name
2026-02-28 23:31:19 +02:00
parent 3b84ebb916
commit 85d98381df
93 changed files with 5940 additions and 0 deletions

View 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 };
}

View 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,
};
}

View 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 };
}