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>
|
||||
<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>" />
|
||||
<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>
|
||||
<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" />
|
||||
|
||||
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 { 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' && (
|
||||
<>
|
||||
<SummaryCards />
|
||||
<MobilFiltreCekmecesi />
|
||||
<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 />
|
||||
<RightSidebar />
|
||||
{/* Sağ panel küçük ekranda listeyi uzatıyordu */}
|
||||
<div className="hidden xl:block shrink-0"><RightSidebar /></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 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 ? (
|
||||
<>
|
||||
<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>
|
||||
{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>
|
||||
|
||||
@@ -8,3 +8,8 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<App />
|
||||
</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