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(() => {}); });
+}