diff --git a/index.html b/index.html index 0921123..56a4b3b 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,14 @@ - + + + + + + + + İhaleist — Akıllı İhale Takip ve Analiz Platformu diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..4bf2858 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..e30fb7d Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..c60a8d0 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/maskable-192.png b/public/icons/maskable-192.png new file mode 100644 index 0000000..36a1146 Binary files /dev/null and b/public/icons/maskable-192.png differ diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png new file mode 100644 index 0000000..fa340d6 Binary files /dev/null and b/public/icons/maskable-512.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..2081e89 --- /dev/null +++ b/public/manifest.webmanifest @@ -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" } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..7d2092c --- /dev/null +++ b/public/sw.js @@ -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('/')))); +}); diff --git a/src/App.jsx b/src/App.jsx index a2dc49c..b8357dd 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -23,6 +23,7 @@ import { ContractSearch } from './components/tools/ContractSearch'; import { VatCalculator } from './components/tools/VatCalculator'; import { SinirDeger } from './components/tools/SinirDeger'; import { KullanimKilavuzu } from './components/KullanimKilavuzu'; +import { MobilFiltreCekmecesi } from './components/MobilFiltreCekmecesi'; import { PriceAnalysis } from './components/tools/PriceAnalysis'; import { CompetitorRadar } from './components/tools/CompetitorRadar'; import { DocumentVault } from './components/tools/DocumentVault'; @@ -48,10 +49,13 @@ const MainContent = () => { {activeTab === 'list' && ( <> +
- + {/* Masaüstünde sabit panel; mobilde çekmece (yukarıda) */} +
- + {/* Sağ panel küçük ekranda listeyi uzatıyordu */} +
)} diff --git a/src/components/MobilFiltreCekmecesi.jsx b/src/components/MobilFiltreCekmecesi.jsx new file mode 100644 index 0000000..ca422be --- /dev/null +++ b/src/components/MobilFiltreCekmecesi.jsx @@ -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 ( +
+ + {acik && ( +
setAcik(false)}> +
+
e.stopPropagation()} className="relative h-full w-[88%] max-w-sm bg-slate-100 shadow-2xl overflow-y-auto p-3 animate-fade-in"> +
+ Filtreler + +
+ +
+
+ )} +
+ ); +}; diff --git a/src/components/tools/PwaSettings.jsx b/src/components/tools/PwaSettings.jsx index 19a0074..701febe 100644 --- a/src/components/tools/PwaSettings.jsx +++ b/src/components/tools/PwaSettings.jsx @@ -57,9 +57,28 @@ export const PwaSettings = () => { }; const setQuiet = (patch) => setReminderSettings((s) => ({ ...s, ...patch })); - const handleInstallPwa = () => { - setInstalled(true); - confetti({ particleCount: 70 }); + // 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 ( @@ -237,7 +256,7 @@ export const PwaSettings = () => { {installed ? ( <> - Cihaza Yüklendi! + Uygulama olarak yüklü ) : ( <> @@ -246,6 +265,13 @@ export const PwaSettings = () => { )} + {iosIpucu && !installed && ( +
+

iPhone / iPad (Safari): alttaki Paylaş simgesi → Ana Ekrana Ekle.

+

Android (Chrome): sağ üst ⋮ menü → Uygulamayı yükle / Ana ekrana ekle.

+

Masaüstü (Chrome/Edge): adres çubuğunun sağındaki yükleme simgesi.

+
+ )}
diff --git a/src/main.jsx b/src/main.jsx index 54b39dd..76ac609 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -8,3 +8,8 @@ ReactDOM.createRoot(document.getElementById('root')).render( , ) + +// 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(() => {}); }); +}