Preserve subscriptions on request failure and isolate account state

This commit is contained in:
2026-09-06 12:55:05 +03:00
parent 0545b54289
commit 0a0c01baf1
6 changed files with 79 additions and 61 deletions
+43 -59
View File
@@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useApp } from '../../context/AppContext';
import { TelegramBaglanti } from './TelegramBaglanti';
import { BriefTercihi } from './BriefTercihi';
import { getToken } from '../../services/auth';
import {accountRequest,requireArray} from '../../services/accountRequest';
import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles, ChevronDown, ChevronUp, X } from 'lucide-react';
@@ -12,6 +13,10 @@ import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles,
* isteğe bağlıdır (varsayılan: tüm Türkiye).
*/
export const NotifySubscriptions = () => {
const {authUser}=useApp();
return authUser ? <SubscriptionForm key={`${authUser.email}:${getToken()}`} /> : null;
};
const SubscriptionForm = () => {
const { authUser, favoriteKurumlar, userProfile, sektorListesi } = useApp();
const [abonelikler, setAbonelikler] = useState(null);
@@ -22,20 +27,22 @@ export const NotifySubscriptions = () => {
const [gelismis, setGelismis] = useState(false);
const [daralt, setDaralt] = useState({ sehir: '', alimTuru: '', favoriKurumlar: false });
const alive=useRef(true),mutation=useRef(false),scan=useRef(false);
// Site tarama
const [site, setSite] = useState(userProfile?.website || '');
const [tarama, setTarama] = useState(null);
const [taraniyor, setTaraniyor] = useState(false);
const [taramaHata, setTaramaHata] = useState(null);
const yukle = () => {
if (!authUser) return;
fetch('/api/notify/subscriptions', { headers: { Authorization: `Bearer ${getToken()}` } })
.then((r) => r.json())
.then((d) => setAbonelikler(d.abonelikler || []))
.catch(() => setAbonelikler([]));
const yukle=async()=>{
if(mutation.current)return;
mutation.current=true;setBusy(true);setError(null);
try {const data=await accountRequest('/api/notify/subscriptions');const rows=requireArray(data,'abonelikler');if(alive.current)setAbonelikler(rows);}
catch(e){if(alive.current)setError(e.message);}
finally {mutation.current=false;if(alive.current)setBusy(false);}
};
useEffect(yukle, [authUser]);
useEffect(()=>{alive.current=true;void yukle();return()=>{alive.current=false;};},[]);
const konuEkle = (k) => {
const t = String(k || '').trim();
@@ -44,56 +51,33 @@ export const NotifySubscriptions = () => {
};
const konuCikar = (k) => setKonular(konular.filter((x) => x !== k));
const siteyiTara = async () => {
setTaraniyor(true); setTaramaHata(null); setTarama(null);
const siteyiTara=async()=>{
if(scan.current)return;scan.current=true;setTaraniyor(true);setTaramaHata(null);setTarama(null);
try {
const res = await fetch('/api/company/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ url: site }),
});
const d = await res.json();
if (!d.success) throw new Error(d.error);
setTarama(d);
} catch (e) {
setTaramaHata(e.message);
} finally { setTaraniyor(false); }
const data=await accountRequest('/api/company/scan',{method:'POST',timeoutMs:120000,body:JSON.stringify({url:site})});
if(!Number.isFinite(data.karakter)||!Number.isFinite(data.tarananSayfa)||!Array.isArray(data.sonuc?.ihaleKonulari))throw new Error('Site tarama sonucu geçersiz.');
if(alive.current)setTarama(data);
}catch(e){if(alive.current)setTaramaHata(e.message);}
finally {scan.current=false;if(alive.current)setTaraniyor(false);}
};
const kaydet = async () => {
if (!konular.length) { setError('En az bir konu seçin.'); return; }
setBusy(true); setError(null);
const kaydet=async()=>{
if(mutation.current)return;
if(!konular.length){setError('En az bir konu seçin.');return;}
mutation.current=true;setBusy(true);setError(null);
try {
const res = await fetch('/api/notify/subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({
kriter: {
konular,
sehir: daralt.sehir,
alimTuru: daralt.alimTuru,
kurumlar: daralt.favoriKurumlar ? favoriteKurumlar : [],
},
}),
});
const d = await res.json();
if (!d.success) throw new Error(d.error);
setAbonelikler(d.abonelikler);
setKonular([]); setDaralt({ sehir: '', alimTuru: '', favoriKurumlar: false });
} catch (e) { setError(e.message); }
finally { setBusy(false); }
const data=await accountRequest('/api/notify/subscriptions',{method:'POST',body:JSON.stringify({kriter:{konular,sehir:daralt.sehir,alimTuru:daralt.alimTuru,kurumlar:daralt.favoriKurumlar?favoriteKurumlar:[]}})});
const rows=requireArray(data,'abonelikler');
if(alive.current){setAbonelikler(rows);setKonular([]);setDaralt({sehir:'',alimTuru:'',favoriKurumlar:false});}
}catch(e){if(alive.current)setError(e.message);}
finally {mutation.current=false;if(alive.current)setBusy(false);}
};
const sil = async (id) => {
setBusy(true);
const sil=async id=>{
if(mutation.current)return;mutation.current=true;setBusy(true);setError(null);
try {
const res = await fetch(`/api/notify/subscriptions?id=${id}`, {
method: 'DELETE', headers: { Authorization: `Bearer ${getToken()}` },
});
const d = await res.json();
setAbonelikler(d.abonelikler || []);
} catch (e) { setError(e.message); }
finally { setBusy(false); }
const data=await accountRequest('/api/notify/subscriptions?'+new URLSearchParams({id}),{method:'DELETE'});
const rows=requireArray(data,'abonelikler');if(alive.current)setAbonelikler(rows);
}catch(e){if(alive.current)setError(e.message);}
finally {mutation.current=false;if(alive.current)setBusy(false);}
};
const ozet = (a) => [
@@ -129,9 +113,9 @@ export const NotifySubscriptions = () => {
</p>
{/* Mevcut kayıtlar */}
{abonelikler === null ? (
{abonelikler === null ? (!error && (
<div className="flex items-center gap-2 text-xs text-slate-400"><Loader2 className="w-3.5 h-3.5 animate-spin" /> yükleniyor…</div>
) : abonelikler.length > 0 && (
)) : abonelikler.length > 0 && (
<div className="space-y-2">
{abonelikler.map((a) => (
<div key={a.id} className="flex items-start justify-between gap-2 bg-slate-50 border border-slate-200 rounded-lg px-3 py-2">
@@ -141,7 +125,7 @@ export const NotifySubscriptions = () => {
{a.sonGonderim ? `son gönderim: ${new Date(a.sonGonderim).toLocaleString('tr-TR')}` : 'ilk tur bekleniyor'}
</p>
</div>
<button onClick={() => sil(a.id)} disabled={busy} className="text-slate-400 hover:text-red-500 shrink-0 mt-0.5" title="Sil">
<button onClick={() => sil(a.id)} disabled={busy} className="text-slate-400 hover:text-red-500 shrink-0 mt-0.5" aria-label={`Aboneliği sil: ${ozet(a)}`} title="Sil">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
@@ -250,16 +234,16 @@ export const NotifySubscriptions = () => {
)}
</div>
<button onClick={kaydet} disabled={busy || !konular.length}
<button onClick={kaydet} disabled={busy || abonelikler===null || !konular.length}
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-2">
{busy ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />}
Bildirimi Kaydet
</button>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
<div role="alert" className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-red-800">{error}</p>
<p className="text-[11px] text-red-800">{error} <button disabled={busy} onClick={yukle} className="underline">Abonelikleri yeniden yükle</button></p>
</div>
)}