feat: mobil uyumlu PWA — manifest, service worker, gercek kurulum, filtre cekmecesi
- public/manifest.webmanifest (standalone, tr, kisayollar) + 192/512 ve maskable ikonlar + apple-touch-icon; index.html PWA meta etiketleri - public/sw.js: /assets cache-first (hash'li adlar), kabuk network-first, /api ASLA onbellekten (eski ihale gosterilmesin); surum degisince temizlik - main.jsx: SW yalniz uretimde kaydedilir (dev'de HMR ile cakismasin) - Mobil PWA sayfasindaki "Cihaza Yukle" dugmesi sahteydi (setInstalled(true)); artik beforeinstallprompt ile gercek kurulum, standalone algilama, iOS/ Android/masaustu yol tarifi - Kucuk ekranda sol filtre paneli listeyi 72 satir asagi itiyordu: lg altinda "Filtreler" dugmesiyle acilan cekmece; sag panel xl altinda gizli Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+8
-1
@@ -3,7 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>" />
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
<meta name="theme-color" content="#1f2937" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="İhaleist" />
|
||||||
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
||||||
<title>İhaleist — Akıllı İhale Takip ve Analiz Platformu</title>
|
<title>İhaleist — Akıllı İhale Takip ve Analiz Platformu</title>
|
||||||
<meta name="description" content="Kamu ve özel sektör ihalelerini tek yerde takip edin; yapay zeka ile uygunluk, risk ve fırsat analizi yapın." />
|
<meta name="description" content="Kamu ve özel sektör ihalelerini tek yerde takip edin; yapay zeka ile uygunluk, risk ve fırsat analizi yapın." />
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "İhaleist — Kamu İhale Takip",
|
||||||
|
"short_name": "İhaleist",
|
||||||
|
"description": "Türkiye kamu ihaleleri: arama, derin şartname analizi, kalem havuzu, uyum skoru, takip ve bildirim.",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "any",
|
||||||
|
"background_color": "#1f2937",
|
||||||
|
"theme_color": "#1f2937",
|
||||||
|
"lang": "tr",
|
||||||
|
"categories": ["business", "productivity"],
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
|
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||||
|
{ "src": "/icons/maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
|
||||||
|
{ "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{ "name": "Ana İhale Listesi", "url": "/", "description": "Açık ihaleler" },
|
||||||
|
{ "name": "Kanban Pano", "url": "/?tab=kanban" },
|
||||||
|
{ "name": "Doküman Deposu", "url": "/?tab=dokuman_deposu" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* İhaleist service worker — uygulama kabuğu çevrimdışı açılır, API her zaman ağdan.
|
||||||
|
Sürüm değişince eski önbellek temizlenir (build her deploy'da yeni asset adı üretir). */
|
||||||
|
const SURUM = 'ihaleist-v1';
|
||||||
|
const KABUK = ['/', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png'];
|
||||||
|
|
||||||
|
self.addEventListener('install', (e) => {
|
||||||
|
e.waitUntil(caches.open(SURUM).then((c) => c.addAll(KABUK)).then(() => self.skipWaiting()));
|
||||||
|
});
|
||||||
|
self.addEventListener('activate', (e) => {
|
||||||
|
e.waitUntil(caches.keys().then((ks) => Promise.all(ks.filter((k) => k !== SURUM).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
|
||||||
|
});
|
||||||
|
self.addEventListener('fetch', (e) => {
|
||||||
|
const url = new URL(e.request.url);
|
||||||
|
if (e.request.method !== 'GET' || url.origin !== self.location.origin) return;
|
||||||
|
// API: yalnız ağ — önbellekten eski ihale gösterme
|
||||||
|
if (url.pathname.startsWith('/api/')) return;
|
||||||
|
// Hash'li build varlıkları: cache-first (değişmez adlar)
|
||||||
|
if (url.pathname.startsWith('/assets/')) {
|
||||||
|
e.respondWith(caches.match(e.request).then((hit) => hit || fetch(e.request).then((r) => {
|
||||||
|
const kopya = r.clone(); caches.open(SURUM).then((c) => c.put(e.request, kopya)); return r;
|
||||||
|
})));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Kabuk / sayfa: network-first, ağ yoksa önbellekten index
|
||||||
|
e.respondWith(fetch(e.request).then((r) => {
|
||||||
|
const kopya = r.clone(); caches.open(SURUM).then((c) => c.put(e.request, kopya)); return r;
|
||||||
|
}).catch(() => caches.match(e.request).then((hit) => hit || caches.match('/'))));
|
||||||
|
});
|
||||||
+6
-2
@@ -23,6 +23,7 @@ import { ContractSearch } from './components/tools/ContractSearch';
|
|||||||
import { VatCalculator } from './components/tools/VatCalculator';
|
import { VatCalculator } from './components/tools/VatCalculator';
|
||||||
import { SinirDeger } from './components/tools/SinirDeger';
|
import { SinirDeger } from './components/tools/SinirDeger';
|
||||||
import { KullanimKilavuzu } from './components/KullanimKilavuzu';
|
import { KullanimKilavuzu } from './components/KullanimKilavuzu';
|
||||||
|
import { MobilFiltreCekmecesi } from './components/MobilFiltreCekmecesi';
|
||||||
import { PriceAnalysis } from './components/tools/PriceAnalysis';
|
import { PriceAnalysis } from './components/tools/PriceAnalysis';
|
||||||
import { CompetitorRadar } from './components/tools/CompetitorRadar';
|
import { CompetitorRadar } from './components/tools/CompetitorRadar';
|
||||||
import { DocumentVault } from './components/tools/DocumentVault';
|
import { DocumentVault } from './components/tools/DocumentVault';
|
||||||
@@ -48,10 +49,13 @@ const MainContent = () => {
|
|||||||
{activeTab === 'list' && (
|
{activeTab === 'list' && (
|
||||||
<>
|
<>
|
||||||
<SummaryCards />
|
<SummaryCards />
|
||||||
|
<MobilFiltreCekmecesi />
|
||||||
<div className="flex flex-col lg:flex-row gap-5 items-start">
|
<div className="flex flex-col lg:flex-row gap-5 items-start">
|
||||||
<LeftSidebar />
|
{/* Masaüstünde sabit panel; mobilde çekmece (yukarıda) */}
|
||||||
|
<div className="hidden lg:block shrink-0"><LeftSidebar /></div>
|
||||||
<TenderList />
|
<TenderList />
|
||||||
<RightSidebar />
|
{/* Sağ panel küçük ekranda listeyi uzatıyordu */}
|
||||||
|
<div className="hidden xl:block shrink-0"><RightSidebar /></div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { SlidersHorizontal, X } from 'lucide-react';
|
||||||
|
import { LeftSidebar } from './LeftSidebar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mobil filtre çekmecesi: küçük ekranda sol menü listeyi aşağı itiyordu
|
||||||
|
* (72 satırlık panel). Artık lg altında gizli; "Filtreler" düğmesiyle
|
||||||
|
* soldan açılan çekmecede aynı bileşen çalışır.
|
||||||
|
*/
|
||||||
|
export const MobilFiltreCekmecesi = () => {
|
||||||
|
const [acik, setAcik] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAcik(true)}
|
||||||
|
className="w-full mb-3 bg-white border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold text-slate-800 flex items-center justify-center gap-2 shadow-xs"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="w-4 h-4 text-blue-600" /> Filtreler & Hızlı Erişim
|
||||||
|
</button>
|
||||||
|
{acik && (
|
||||||
|
<div className="fixed inset-0 z-50 flex" onClick={() => setAcik(false)}>
|
||||||
|
<div className="absolute inset-0 bg-slate-900/50" />
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className="relative h-full w-[88%] max-w-sm bg-slate-100 shadow-2xl overflow-y-auto p-3 animate-fade-in">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-xs font-black text-slate-800 uppercase tracking-wider">Filtreler</span>
|
||||||
|
<button onClick={() => setAcik(false)} className="p-1.5 rounded-lg bg-white border border-slate-200" aria-label="Kapat"><X className="w-4 h-4" /></button>
|
||||||
|
</div>
|
||||||
|
<LeftSidebar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -57,9 +57,28 @@ export const PwaSettings = () => {
|
|||||||
};
|
};
|
||||||
const setQuiet = (patch) => setReminderSettings((s) => ({ ...s, ...patch }));
|
const setQuiet = (patch) => setReminderSettings((s) => ({ ...s, ...patch }));
|
||||||
|
|
||||||
const handleInstallPwa = () => {
|
// Gerçek PWA kurulumu: tarayıcı beforeinstallprompt verirse yerel diyalog;
|
||||||
setInstalled(true);
|
// zaten standalone açıldıysa "yüklü"; iOS Safari'de olay yok → yol tarifi.
|
||||||
confetti({ particleCount: 70 });
|
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 (
|
return (
|
||||||
@@ -237,7 +256,7 @@ export const PwaSettings = () => {
|
|||||||
{installed ? (
|
{installed ? (
|
||||||
<>
|
<>
|
||||||
<CheckCircle2 className="w-4 h-4 text-emerald-950" />
|
<CheckCircle2 className="w-4 h-4 text-emerald-950" />
|
||||||
<span>Cihaza Yüklendi!</span>
|
<span>Uygulama olarak yüklü</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -246,6 +265,13 @@ export const PwaSettings = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,3 +8,8 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PWA: service worker yalnız üretimde (dev'de HMR ile çakışmasın)
|
||||||
|
if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
||||||
|
window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => {}); });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user