309 lines
16 KiB
React
309 lines
16 KiB
React
import React, { useState, useEffect } from 'react';
|
||
import { useApp } from '../../context/AppContext';
|
||
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, Moon, Mail, Send } from 'lucide-react';
|
||
import confetti from 'canvas-confetti';
|
||
import { getToken } from '../../services/auth';
|
||
import { NotifySubscriptions } from './NotifySubscriptions';
|
||
|
||
export const PwaSettings = () => {
|
||
const { userProfile, authUser, tenders, reminderSettings, setReminderSettings, requestPushPermission, notify, isQuietNow } = useApp();
|
||
// Gerçek Cache API sayımı — sahte "102 ihale önbellekte" yerine
|
||
const [onbellekSayisi, setOnbellekSayisi] = useState(null);
|
||
const [onbellekAdi, setOnbellekAdi] = useState('');
|
||
const [cacheLoading, setCacheLoading] = useState(false);
|
||
const [cacheError, setCacheError] = useState(null);
|
||
const [online, setOnline] = useState(navigator.onLine);
|
||
const refreshCacheStatus = async () => {
|
||
setCacheLoading(true); setCacheError(null);
|
||
try {
|
||
if (!('caches' in window)) throw new Error('Bu tarayıcı uygulama önbelleğini desteklemiyor.');
|
||
const adlar = (await caches.keys()).filter(name => name.startsWith('ihaleist-'));
|
||
let toplam = 0;
|
||
for (const ad of adlar) toplam += (await (await caches.open(ad)).keys()).length;
|
||
setOnbellekSayisi(toplam); setOnbellekAdi(adlar.join(', '));
|
||
} catch (error) { setCacheError(error.message); setOnbellekSayisi(null); }
|
||
finally { setCacheLoading(false); }
|
||
};
|
||
useEffect(() => { refreshCacheStatus(); }, []);
|
||
useEffect(() => {
|
||
const changed = () => setOnline(navigator.onLine);
|
||
window.addEventListener('online', changed); window.addEventListener('offline', changed);
|
||
return () => { window.removeEventListener('online', changed); window.removeEventListener('offline', changed); };
|
||
}, []);
|
||
const [installed, setInstalled] = useState(false);
|
||
const [permission, setPermission] = useState(typeof Notification !== 'undefined' ? Notification.permission : 'unsupported');
|
||
|
||
// E-posta (Resend) durumu + test gönderimi
|
||
const [emailCfg, setEmailCfg] = useState(null); // null=bilinmiyor, true/false
|
||
const [emailSending, setEmailSending] = useState(false);
|
||
const [emailResult, setEmailResult] = useState(null);
|
||
useEffect(() => {
|
||
fetch('/api/notify/email/config')
|
||
.then((r) => r.json())
|
||
.then((d) => setEmailCfg(Boolean(d.configured)))
|
||
.catch(() => setEmailCfg(false));
|
||
}, []);
|
||
|
||
const sendTestEmail = async () => {
|
||
setEmailSending(true);
|
||
setEmailResult(null);
|
||
try {
|
||
const yeniler = tenders.filter((t) => t.isYeni).slice(0, 15);
|
||
const res = await fetch('/api/notify/email/test', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
|
||
body: JSON.stringify({ subject: 'İhaleist — bildirim testi', tenders: yeniler }),
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
setEmailResult({ ok: true, msg: `E-posta gönderildi → ${data.to}` });
|
||
notify('E-posta gönderildi', `Test e-postası ${data.to} adresine iletildi.`);
|
||
} else {
|
||
setEmailResult({ ok: false, msg: data.error || 'Gönderilemedi' });
|
||
}
|
||
} catch (e) {
|
||
setEmailResult({ ok: false, msg: e.message });
|
||
} finally {
|
||
setEmailSending(false);
|
||
}
|
||
};
|
||
|
||
const handleEnablePush = async () => {
|
||
const p = await requestPushPermission();
|
||
setPermission(p);
|
||
if (p === 'granted') {
|
||
setReminderSettings((s) => ({ ...s, pushEnabled: true }));
|
||
notify('Bildirimler açıldı', 'Yeni ihale ve son tarih uyarıları buradan gelecek.');
|
||
confetti({ particleCount: 50 });
|
||
}
|
||
};
|
||
const setQuiet = (patch) => setReminderSettings((s) => ({ ...s, ...patch }));
|
||
|
||
// Gerçek PWA kurulumu: tarayıcı beforeinstallprompt verirse yerel diyalog;
|
||
// zaten standalone açıldıysa "yüklü"; iOS Safari'de olay yok → yol tarifi.
|
||
const [kurulumOlayi, setKurulumOlayi] = useState(null);
|
||
const [iosIpucu, setIosIpucu] = useState(false);
|
||
useEffect(() => {
|
||
const standalone = window.matchMedia?.('(display-mode: standalone)')?.matches || window.navigator.standalone === true;
|
||
if (standalone) setInstalled(true);
|
||
const yakala = (e) => { e.preventDefault(); setKurulumOlayi(e); };
|
||
const kuruldu = () => { setInstalled(true); setKurulumOlayi(null); };
|
||
window.addEventListener('beforeinstallprompt', yakala);
|
||
window.addEventListener('appinstalled', kuruldu);
|
||
return () => { window.removeEventListener('beforeinstallprompt', yakala); window.removeEventListener('appinstalled', kuruldu); };
|
||
}, []);
|
||
const handleInstallPwa = async () => {
|
||
if (kurulumOlayi) {
|
||
kurulumOlayi.prompt();
|
||
const { outcome } = await kurulumOlayi.userChoice.catch(() => ({ outcome: 'dismissed' }));
|
||
if (outcome === 'accepted') setInstalled(true);
|
||
setKurulumOlayi(null);
|
||
return;
|
||
}
|
||
setIosIpucu(true); // olay yoksa (iOS / zaten yüklü / desteklenmiyor) yol tarifi göster
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6 animate-fade-in">
|
||
|
||
{/* Header Banner */}
|
||
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-cyan-900 text-white p-6 rounded-2xl shadow-lg border border-cyan-800/40 flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 rounded-2xl bg-cyan-600 text-white flex items-center justify-center font-black shadow-md">
|
||
<Smartphone className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<h2 className="font-extrabold text-xl">Mobil uygulama ve bağlantı durumu</h2>
|
||
<p className="text-xs text-cyan-200 mt-0.5">Uygulama kabuğu önbellekten açılabilir. İhale güncelleme, belge erişimi ve hazırlık kaydı için internet bağlantısı gerekir.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Controls Grid */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
|
||
{/* Offline Cache Status */}
|
||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
||
{online ? <Wifi className="w-4 h-4 text-emerald-600" /> : <WifiOff className="w-4 h-4 text-slate-400" />} Çevrimdışı (Offline) Önbellek
|
||
</h3>
|
||
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full">
|
||
{online ? 'Bağlantı var' : 'Çevrimdışı'}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-xs text-slate-700">
|
||
<p className="flex justify-between">
|
||
<span>İhale Kayıtları (bellekte):</span>
|
||
<strong className="text-slate-900">{tenders.length.toLocaleString('tr-TR')} ihale</strong>
|
||
</p>
|
||
<p className="flex justify-between">
|
||
<span>Önbelleğe alınmış istek:</span>
|
||
<strong className="text-slate-900">{onbellekSayisi == null ? '—' : `${onbellekSayisi} kayıt`}</strong>
|
||
</p>
|
||
<p className="flex justify-between">
|
||
<span>Uygulama sürümü önbelleği:</span>
|
||
<strong className="text-emerald-700">{onbellekAdi || '—'}</strong>
|
||
</p>
|
||
</div>
|
||
|
||
<button
|
||
onClick={refreshCacheStatus} disabled={cacheLoading}
|
||
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold text-xs py-2.5 rounded-xl border border-slate-200 transition-colors"
|
||
>
|
||
{cacheLoading ? 'Kontrol ediliyor…' : 'Önbellek durumunu kontrol et'}
|
||
</button>
|
||
{cacheError && <p role="alert" className="text-xs text-red-700">{cacheError}</p>}
|
||
<p className="text-xs text-slate-600">Bağlantı durumu sunucuya erişimi garanti etmez. Çevrimdışı değişiklikler otomatik gönderim kuyruğuna alınmaz; kayıt sonucunu kontrol edin.</p>
|
||
</div>
|
||
|
||
{/* Gerçek Tarayıcı Bildirimleri + Sessiz Saatler */}
|
||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
||
<Bell className="w-4 h-4 text-amber-500" /> Anlık Bildirimler
|
||
</h3>
|
||
<span className={`font-extrabold text-[10px] px-2 py-0.5 rounded-full ${
|
||
permission === 'granted' ? 'bg-emerald-100 text-emerald-800' : permission === 'denied' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-600'
|
||
}`}>
|
||
{permission === 'granted' ? 'İzin Verildi' : permission === 'denied' ? 'Reddedildi' : permission === 'unsupported' ? 'Desteklenmiyor' : 'İzin Bekliyor'}
|
||
</span>
|
||
</div>
|
||
|
||
<p className="text-xs text-slate-600 leading-relaxed">
|
||
Tarayıcı bildirimlerine izin verin; yeni ihale ve son tarih uyarıları gerçek bildirim olarak gelir.
|
||
</p>
|
||
|
||
{permission !== 'granted' ? (
|
||
<button onClick={handleEnablePush} disabled={permission === 'unsupported'}
|
||
className="w-full bg-amber-500 hover:bg-amber-400 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl">
|
||
Bildirimlere İzin Ver
|
||
</button>
|
||
) : (
|
||
<button onClick={() => notify('Test bildirimi', isQuietNow() ? 'Sessiz saatte — sadece listeye eklendi.' : 'Bildirimler çalışıyor ✓')}
|
||
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs py-2.5 rounded-xl">
|
||
Test Bildirimi Gönder
|
||
</button>
|
||
)}
|
||
|
||
{/* Sessiz saatler — gerçek */}
|
||
<div className="pt-2 border-t border-slate-100 space-y-2">
|
||
<label className="flex items-center justify-between text-xs font-bold text-slate-800 cursor-pointer">
|
||
<span className="flex items-center gap-1.5"><Moon className="w-3.5 h-3.5 text-indigo-500" /> Sessiz Saatler</span>
|
||
<input type="checkbox" checked={reminderSettings.quietEnabled}
|
||
onChange={(e) => setQuiet({ quietEnabled: e.target.checked })}
|
||
className="w-4 h-4 accent-indigo-600 rounded" />
|
||
</label>
|
||
{reminderSettings.quietEnabled && (
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<input type="time" value={reminderSettings.quietStart}
|
||
onChange={(e) => setQuiet({ quietStart: e.target.value })}
|
||
className="bg-slate-50 border border-slate-300 rounded-lg px-2 py-1.5 flex-1" />
|
||
<span className="text-slate-400">→</span>
|
||
<input type="time" value={reminderSettings.quietEnd}
|
||
onChange={(e) => setQuiet({ quietEnd: e.target.value })}
|
||
className="bg-slate-50 border border-slate-300 rounded-lg px-2 py-1.5 flex-1" />
|
||
</div>
|
||
)}
|
||
{reminderSettings.quietEnabled && (
|
||
<p className="text-[10px] text-slate-400">
|
||
{isQuietNow() ? '🔕 Şu an sessiz saatte — tarayıcı bildirimi gösterilmez, uyarılar listeye eklenir.' : '🔔 Şu an aktif — bildirimler gösterilir.'}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* E-posta Bildirimi (Resend) — gerçek gönderim */}
|
||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
||
<Mail className="w-4 h-4 text-blue-500" /> E-posta Bildirimi
|
||
</h3>
|
||
<span className={`font-extrabold text-[10px] px-2 py-0.5 rounded-full ${
|
||
emailCfg === true ? 'bg-emerald-100 text-emerald-800' : emailCfg === false ? 'bg-amber-100 text-amber-800' : 'bg-slate-100 text-slate-600'
|
||
}`}>
|
||
{emailCfg === true ? 'Aktif (Resend)' : emailCfg === false ? 'Yapılandırılmadı' : 'Kontrol ediliyor…'}
|
||
</span>
|
||
</div>
|
||
|
||
<p className="text-xs text-slate-600 leading-relaxed">
|
||
{emailCfg === false
|
||
? 'E-posta göndermek için sunucuya RESEND_API_KEY ekleyin (Coolify → Environment). Ayrıca gönderen domainini Resend’de DNS ile doğrulayın.'
|
||
: 'Takip kriterlerinize uyan ihaleler, hesabınızın e-posta adresine özet olarak gönderilir. Aşağıdan kendinize test gönderebilirsiniz.'}
|
||
</p>
|
||
|
||
<label className="flex items-center justify-between text-xs font-bold text-slate-800 cursor-pointer">
|
||
<span>E-posta bildirimleri açık</span>
|
||
<input type="checkbox" checked={reminderSettings.emailEnabled}
|
||
onChange={(e) => setReminderSettings((s) => ({ ...s, emailEnabled: e.target.checked }))}
|
||
className="w-4 h-4 accent-blue-600 rounded" />
|
||
</label>
|
||
|
||
<button
|
||
onClick={sendTestEmail}
|
||
disabled={emailSending || emailCfg !== true || !authUser}
|
||
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-2"
|
||
title={!authUser ? 'Giriş yapın' : emailCfg !== true ? 'RESEND_API_KEY gerekli' : 'Kendinize test e-postası gönder'}
|
||
>
|
||
<Send className="w-3.5 h-3.5" />
|
||
{emailSending ? 'Gönderiliyor…' : 'Kendime Test E-postası Gönder'}
|
||
</button>
|
||
|
||
{emailResult && (
|
||
<p className={`text-[11px] ${emailResult.ok ? 'text-emerald-600' : 'text-red-600'}`}>
|
||
{emailResult.ok ? '✓ ' : '⚠ '}{emailResult.msg}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Install PWA Application */}
|
||
<div className="bg-linear-to-br from-slate-900 to-cyan-950 text-white p-6 rounded-2xl shadow-xs space-y-4 border border-cyan-800/40">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="font-black text-sm text-white flex items-center gap-2">
|
||
<Download className="w-4 h-4 text-cyan-400" /> İhaleist Mobil / Masaüstü
|
||
</h3>
|
||
<span className="bg-cyan-500 text-slate-950 font-black text-[10px] px-2 py-0.5 rounded-full">
|
||
PWA 2.0
|
||
</span>
|
||
</div>
|
||
|
||
<p className="text-xs text-slate-300 leading-relaxed">
|
||
İhaleist'i iOS, Android, Windows veya Mac cihazınıza bir tıklama ile uygulama olarak kurun.
|
||
</p>
|
||
|
||
<button
|
||
onClick={handleInstallPwa}
|
||
disabled={installed}
|
||
className="w-full bg-cyan-500 hover:bg-cyan-400 disabled:bg-slate-700 text-slate-950 font-black text-xs py-3 rounded-xl flex items-center justify-center gap-2 shadow-md transition-colors cursor-pointer"
|
||
>
|
||
{installed ? (
|
||
<>
|
||
<CheckCircle2 className="w-4 h-4 text-emerald-950" />
|
||
<span>Uygulama olarak yüklü</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Download className="w-4 h-4 text-slate-950" />
|
||
<span>İhaleist Uygulamasını Cihaza Yükle</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
{iosIpucu && !installed && (
|
||
<div className="bg-white/10 border border-white/15 rounded-xl p-3 text-[11px] text-slate-200 leading-relaxed space-y-1">
|
||
<p><strong className="text-white">iPhone / iPad (Safari):</strong> alttaki Paylaş simgesi → <strong>Ana Ekrana Ekle</strong>.</p>
|
||
<p><strong className="text-white">Android (Chrome):</strong> sağ üst ⋮ menü → <strong>Uygulamayı yükle</strong> / Ana ekrana ekle.</p>
|
||
<p><strong className="text-white">Masaüstü (Chrome/Edge):</strong> adres çubuğunun sağındaki yükleme simgesi.</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{/* Otomatik bildirim abonelikleri */}
|
||
<NotifySubscriptions />
|
||
|
||
</div>
|
||
);
|
||
};
|