// components.jsx — Shared UI: ListingCard variants, Nav, Filters, TrustBar, etc. const { useState, useEffect, useRef, useMemo } = React; // ───────────────────────────────────────────────────────────── // NotifBell — bildirim ikonu + dropdown + polling // ───────────────────────────────────────────────────────────── function NotifBell({ L, lang }) { const [items, setItems] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const ref = useRef(null); const unread = items.filter(n => !n.is_read).length; // Polling useEffect(() => { let alive = true; const fetch = async () => { if (!window.api || !window.api.isLoggedIn()) return; try { const res = await window.api.notifications.list(); if (alive && res?.data) setItems(res.data); } catch {} }; fetch(); const id = setInterval(fetch, 30000); // 30sn return () => { alive = false; clearInterval(id); }; }, []); // Dış tıklama kapatma useEffect(() => { if (!open) return; const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [open]); const onOpen = async () => { setOpen(o => !o); if (!open && unread > 0 && window.api?.isLoggedIn()) { try { await window.api.notifications.readAll(); setItems(prev => prev.map(n => ({ ...n, is_read: true }))); } catch {} } }; // Demo verisi (login değilse veya API boşsa) const demoItems = !window.api?.isLoggedIn() ? [ { id: 'd1', type: 'message', title: lang === 'tr' ? 'Yeni mesaj' : 'New message', body: lang === 'tr' ? '@citytrails sana yazdı' : '@citytrails messaged you', created_at: new Date(Date.now() - 5*60000).toISOString(), is_read: false, link: 'panelim.html#mesajlar' }, { id: 'd2', type: 'order', title: lang === 'tr' ? 'Sipariş onayı' : 'Order confirmed', body: lang === 'tr' ? '@velvet.looks · ödeme alındı' : '@velvet.looks · payment received', created_at: new Date(Date.now() - 2*3600000).toISOString(), is_read: false, link: 'panelim.html#siparisler' }, { id: 'd3', type: 'system', title: lang === 'tr' ? 'Hoş geldin' : 'Welcome', body: lang === 'tr' ? 'InstaSatış\'a hoş geldin!' : 'Welcome to InstaSatış!', created_at: new Date(Date.now() - 24*3600000).toISOString(), is_read: true, link: null }, ] : []; const display = items.length ? items : demoItems; const unreadDisplay = display.filter(n => !n.is_read).length; const fmtTime = (iso) => { const d = new Date(iso); const diff = (Date.now() - d.getTime()) / 1000; if (diff < 60) return lang === 'tr' ? 'şimdi' : 'now'; if (diff < 3600) return Math.floor(diff/60) + (lang === 'tr' ? ' dk' : 'm'); if (diff < 86400) return Math.floor(diff/3600) + (lang === 'tr' ? ' sa' : 'h'); return Math.floor(diff/86400) + (lang === 'tr' ? ' g' : 'd'); }; const iconFor = (t) => ({ message: 'mail', order: 'tag', listing: 'sparkle', system: 'bell', payment: 'tag' }[t] || 'bell'); return (
{open && (
{lang === 'tr' ? 'Bildirimler' : 'Notifications'}
{unreadDisplay > 0 &&
{unreadDisplay} {lang === 'tr' ? 'YENİ' : 'NEW'}
}
{display.length === 0 ? (
{lang === 'tr' ? 'Henüz bildirim yok' : 'No notifications yet'}
) : (
{display.map(n => (
{n.title}
{fmtTime(n.created_at)}
{n.body}
{!n.is_read && }
))}
)} {lang === 'tr' ? 'Tümünü Gör' : 'View All'}
)}
); } // ───────────────────────────────────────────────────────────── // TopNav — used in desktop ekranlarda // ───────────────────────────────────────────────────────────── function TopNav({ L, lang, setLang, active, onNav, onAuth, compact }) { const live = typeof window !== 'undefined' && window.__IS_LIVE_SITE; const items = [ { id: 'home', label: L.nav_explore, href: 'index.html' }, { id: 'compare', label: lang === 'en' ? 'Compare' : 'Karşılaştır', href: 'karsilastir.html' }, { id: 'how', label: L.nav_how, href: 'aged-hesap-nedir.html' }, { id: 'about', label: L.nav_about, href: 'hakkinda.html' }, ]; // Logged-in user state (read from api-client.js) const [me, setMe] = useState(() => (typeof window !== 'undefined' && window.api?.getUser?.()) || null); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { if (!window.api) return; const sync = () => setMe(window.api.getUser?.() || null); // Refresh on focus / storage change window.addEventListener('focus', sync); window.addEventListener('storage', sync); return () => { window.removeEventListener('focus', sync); window.removeEventListener('storage', sync); }; }, []); const logout = async () => { try { await window.api?.auth?.logout?.(); } catch {} setMe(null); if (live) window.location.href = 'index.html'; }; const handleAuth = (m) => { if (onAuth) return onAuth(m); if (live) window.location.href = 'giris.html'; }; return (
!live && onNav && onNav('home')} style={{ cursor: 'pointer', textDecoration: 'none' }}>
{live && } { if (!live) { e.preventDefault(); onNav && onNav('sell'); } }} className="is-btn" style={{ padding: '9px 14px', textDecoration: 'none', background: 'var(--ink)', color: 'var(--bg)', border: 0, fontWeight: 600, gap: 6, }}> {L.nav_sell || 'İlan Ver'} {me ? (
{menuOpen && (
{me.name || '—'}
{me.email}
{[ { label: 'Panelim', href: 'panelim.html', icon: 'grid' }, { label: 'İlanlarım', href: 'panelim.html?tab=listings', icon: 'tag' }, { label: 'Mesajlar', href: 'panelim.html?tab=messages', icon: 'mail' }, { label: 'Güvenlik', href: 'panelim.html?tab=security', icon: 'shield' }, ].map(it => ( {it.label} ))}
)}
) : ( <> )}
); } // ───────────────────────────────────────────────────────────── // TrustBar — small inline row of trust signals // ───────────────────────────────────────────────────────────── function TrustBar({ L, compact = false }) { const items = [ { icon: 'lock', label: L.trust_ssl }, { icon: 'tag', label: L.trust_card }, { icon: 'shield', label: L.trust_iban }, { icon: 'check', label: L.trust_refund }, ]; return (
{items.map((it, i) => (
{it.label}
))}
); } // ───────────────────────────────────────────────────────────── // Badge (NEW / HOT / RARE) // ───────────────────────────────────────────────────────────── function StatusBadge({ kind, L }) { if (!kind) return null; const map = { new: { bg: 'var(--success)', label: L.new_badge, icon: 'sparkle' }, hot: { bg: 'var(--danger)', label: L.hot_badge, icon: 'flame' }, rare: { bg: 'var(--grad)', label: L.rare_badge, icon: 'star' }, }; const m = map[kind]; if (!m) return null; return ( {m.label} ); } // ───────────────────────────────────────────────────────────── // ListingCard — 3 layout variants // layout='grid' → square post grid + stats underneath (default) // layout='passport' → left info / right post mosaic (novel, "passport") // layout='stat' → stat radar hero, minimal posts (numbers-forward) // ───────────────────────────────────────────────────────────── function ListingCard({ listing, L, lang, layout = 'grid', onView, onToggleFav, isFav }) { const l = listing; const nicheLabel = L[`niche_${l.niche}`] || l.niche; if (layout === 'passport') { return (
onView && onView(l)}>
{l.handle} {l.verified && }
{nicheLabel.toUpperCase()} · {l.country}
{ e.stopPropagation(); onToggleFav && onToggleFav(l.id); }}/>
{L.price_ttl.toUpperCase()}
{fmtPrice(l.price, lang)}
); } if (layout === 'stat') { return (
onView && onView(l)}>
{l.handle} {l.verified && }
{nicheLabel.toUpperCase()}
{ e.stopPropagation(); onToggleFav && onToggleFav(l.id); }}/>
{/* Big gradient number */}
{fmtNum(l.followers)}
{L.card_followers}
{Array.from({ length: 9 }).map((_, i) => { const active = i < Math.round((l.er / 10) * 9); return
; })}
ER {l.er.toFixed(1)}% {fmtAge(l.age, lang, L)}
{fmtPrice(l.price, lang)}
); } // default: grid return (
onView && onView(l)}>
{ e.stopPropagation(); onToggleFav && onToggleFav(l.id); }}/>
{l.handle} {l.verified && }
{nicheLabel.toUpperCase()}
{fmtPrice(l.price, lang)}
); } function Stat({ icon, value, label }) { return (
{label}
{value}
); } function MiniStat({ icon, value }) { return (
{value}
); } function FavBtn({ active, onClick }) { return ( ); } // ───────────────────────────────────────────────────────────── // Filter sidebar // ───────────────────────────────────────────────────────────── function FilterPanel({ L, filters, setFilters, compact = false }) { const toggle = (key, v) => setFilters(f => ({ ...f, [key]: f[key] === v ? null : v })); const niches = ['fashion','food','travel','meme','sport','tech','art','lifestyle','music','beauty','fitness','auto']; const ages = [{k:'any', label: L.age_any},{k:'1', label: L.age_1},{k:'3', label: L.age_3},{k:'5', label: L.age_5}]; return ( ); } function FilterSection({ label, children }) { return (
{label}
{children}
); } function filterChipStyle(active) { return { padding: '8px 12px', borderRadius: 999, border: 0, fontSize: 12, fontWeight: 500, cursor: 'pointer', background: active ? 'var(--grad)' : 'var(--chip)', color: active ? '#fff' : 'var(--ink-2)', fontFamily: 'var(--font-body)', display: 'inline-flex', alignItems: 'center', }; } function PriceSlider({ value, onChange }) { return (
onChange(+e.target.value)} style={{ width: '100%', accentColor: 'oklch(0.66 0.22 350)' }}/>
2.000 ₺ ≤ {fmtPrice(value, 'tr')} 35.000 ₺
); } // ───────────────────────────────────────────────────────────── // SortBar // ───────────────────────────────────────────────────────────── function SortBar({ L, sort, setSort, count, layout, setLayout }) { const opts = [ { k: 'new', label: L.sort_new }, { k: 'price_low', label: L.sort_price_low }, { k: 'price_high', label: L.sort_price_high }, { k: 'followers', label: L.sort_followers }, { k: 'age', label: L.sort_age }, ]; return (
{count} {L.stat_listings}
{opts.map(o => ( ))}
{setLayout && (
{['grid','passport','stat'].map(l => ( ))}
)}
); } // ───────────────────────────────────────────────────────────── // ScreenFrame — consistent wrapper for each artboard // ───────────────────────────────────────────────────────────── function ScreenFrame({ children, width, height, theme, label }) { return (
{children}
); } Object.assign(window, { TopNav, TrustBar, StatusBadge, ListingCard, Stat, MiniStat, FavBtn, FilterPanel, FilterSection, PriceSlider, SortBar, ScreenFrame, NotifBell, });