ميزة: تحديث صفحات الخصوصية والشروط مع تاريخ آخر تحديث ثابت وفترة احتفاظ ديناميكية بالملفات

ميزة: إضافة خدمة تحليلات لتكامل Google Analytics

اختبار: تحديث اختبارات خدمة واجهة برمجة التطبيقات (API) لتعكس تغييرات نقاط النهاية

إصلاح: تعديل خدمة واجهة برمجة التطبيقات (API) لدعم تحميل ملفات متعددة ومصادقة المستخدم

ميزة: تطبيق مخزن مصادقة باستخدام Zustand لإدارة المستخدمين

إصلاح: تحسين إعدادات Nginx لتعزيز الأمان ودعم التحليلات
This commit is contained in:
Your Name
2026-03-07 11:14:05 +02:00
parent cfbcc8bd79
commit 0ad2ba0f02
73 changed files with 4696 additions and 462 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react';
import { useAuthStore } from '@/stores/authStore';
interface AdSlotProps {
/** AdSense ad slot ID */
@@ -21,21 +22,50 @@ export default function AdSlot({
responsive = true,
className = '',
}: AdSlotProps) {
const user = useAuthStore((s) => s.user);
const adRef = useRef<HTMLModElement>(null);
const isLoaded = useRef(false);
const clientId = (import.meta.env.VITE_ADSENSE_CLIENT_ID || '').trim();
const slotMap: Record<string, string | undefined> = {
'home-top': import.meta.env.VITE_ADSENSE_SLOT_HOME_TOP,
'home-bottom': import.meta.env.VITE_ADSENSE_SLOT_HOME_BOTTOM,
'top-banner': import.meta.env.VITE_ADSENSE_SLOT_TOP_BANNER,
'bottom-banner': import.meta.env.VITE_ADSENSE_SLOT_BOTTOM_BANNER,
};
const resolvedSlot = /^\d+$/.test(slot) ? slot : slotMap[slot];
useEffect(() => {
if (isLoaded.current) return;
if (isLoaded.current || !clientId || !resolvedSlot) return;
const existingScript = document.querySelector<HTMLScriptElement>(
`script[data-adsense-client="${clientId}"]`
);
if (!existingScript) {
const script = document.createElement('script');
script.async = true;
script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${clientId}`;
script.crossOrigin = 'anonymous';
script.setAttribute('data-adsense-client', clientId);
document.head.appendChild(script);
}
try {
// Push ad to AdSense queue
const adsbygoogle = (window as any).adsbygoogle || [];
const adsWindow = window as Window & { adsbygoogle?: unknown[] };
const adsbygoogle = adsWindow.adsbygoogle || [];
adsbygoogle.push({});
adsWindow.adsbygoogle = adsbygoogle;
isLoaded.current = true;
} catch {
// AdSense not loaded (e.g., ad blocker)
}
}, []);
}, [clientId, resolvedSlot]);
if (!clientId || !resolvedSlot) return null;
// Pro users see no ads
if (user?.plan === 'pro') return null;
return (
<div className={`ad-slot ${className}`}>
@@ -43,8 +73,8 @@ export default function AdSlot({
ref={adRef}
className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client={import.meta.env.VITE_ADSENSE_CLIENT_ID || ''}
data-ad-slot={slot}
data-ad-client={clientId}
data-ad-slot={resolvedSlot}
data-ad-format={format}
data-full-width-responsive={responsive ? 'true' : 'false'}
/>

View File

@@ -1,8 +1,8 @@
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { FileText, Moon, Sun, Menu, X, ChevronDown } from 'lucide-react';
// ...existing code...
import { FileText, Moon, Sun, Menu, X, ChevronDown, UserRound } from 'lucide-react';
import { useAuthStore } from '@/stores/authStore';
interface LangOption {
code: string;
label: string;
@@ -40,6 +40,7 @@ function useDarkMode() {
export default function Header() {
const { t, i18n } = useTranslation();
const { isDark, toggle: toggleDark } = useDarkMode();
const user = useAuthStore((state) => state.user);
const [langOpen, setLangOpen] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const langRef = useRef<HTMLDivElement>(null);
@@ -85,10 +86,24 @@ export default function Header() {
>
{t('common.about')}
</Link>
<Link
to="/account"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
>
{t('common.account')}
</Link>
</nav>
{/* Actions */}
<div className="flex items-center gap-2">
<Link
to="/account"
className="hidden max-w-[220px] items-center gap-2 rounded-xl border border-slate-200 px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 md:flex dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
<UserRound className="h-4 w-4" />
<span className="truncate">{user?.email || t('common.account')}</span>
</Link>
{/* Dark Mode Toggle */}
<button
onClick={toggleDark}
@@ -167,6 +182,13 @@ export default function Header() {
>
{t('common.about')}
</Link>
<Link
to="/account"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{user?.email || t('common.account')}
</Link>
</nav>
)}
</header>