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:
53
frontend/src/components/layout/AdSlot.tsx
Normal file
53
frontend/src/components/layout/AdSlot.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
interface AdSlotProps {
|
||||
/** AdSense ad slot ID */
|
||||
slot: string;
|
||||
/** Ad format: 'auto', 'rectangle', 'horizontal', 'vertical' */
|
||||
format?: string;
|
||||
/** Responsive mode */
|
||||
responsive?: boolean;
|
||||
/** Additional CSS class */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google AdSense ad slot component.
|
||||
* Loads the ad unit once and handles cleanup.
|
||||
*/
|
||||
export default function AdSlot({
|
||||
slot,
|
||||
format = 'auto',
|
||||
responsive = true,
|
||||
className = '',
|
||||
}: AdSlotProps) {
|
||||
const adRef = useRef<HTMLModElement>(null);
|
||||
const isLoaded = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded.current) return;
|
||||
|
||||
try {
|
||||
// Push ad to AdSense queue
|
||||
const adsbygoogle = (window as any).adsbygoogle || [];
|
||||
adsbygoogle.push({});
|
||||
isLoaded.current = true;
|
||||
} catch {
|
||||
// AdSense not loaded (e.g., ad blocker)
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`ad-slot ${className}`}>
|
||||
<ins
|
||||
ref={adRef}
|
||||
className="adsbygoogle"
|
||||
style={{ display: 'block' }}
|
||||
data-ad-client={import.meta.env.VITE_ADSENSE_CLIENT_ID || ''}
|
||||
data-ad-slot={slot}
|
||||
data-ad-format={format}
|
||||
data-full-width-responsive={responsive ? 'true' : 'false'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
frontend/src/components/layout/Footer.tsx
Normal file
45
frontend/src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
export default function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<footer className="border-t border-slate-200 bg-slate-50">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
<FileText className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">
|
||||
© {new Date().getFullYear()} {t('common.appName')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex items-center gap-6">
|
||||
<Link
|
||||
to="/privacy"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600"
|
||||
>
|
||||
{t('common.privacy')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/terms"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600"
|
||||
>
|
||||
{t('common.terms')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/about"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600"
|
||||
>
|
||||
{t('common.about')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
50
frontend/src/components/layout/Header.tsx
Normal file
50
frontend/src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FileText, Globe } from 'lucide-react';
|
||||
|
||||
export default function Header() {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language === 'ar' ? 'en' : 'ar';
|
||||
i18n.changeLanguage(newLang);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white/80 backdrop-blur-lg">
|
||||
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center gap-2 text-xl font-bold text-primary-600">
|
||||
<FileText className="h-7 w-7" />
|
||||
<span>{t('common.appName')}</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="hidden items-center gap-6 md:flex">
|
||||
<Link
|
||||
to="/"
|
||||
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600"
|
||||
>
|
||||
{t('common.home')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/about"
|
||||
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600"
|
||||
>
|
||||
{t('common.about')}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Language Toggle */}
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100"
|
||||
aria-label={t('common.language')}
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
<span>{i18n.language === 'ar' ? 'English' : 'العربية'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user