// 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'}
) : (
)}
{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 (
);
}
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 (
);
}
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 (
);
}
// ─────────────────────────────────────────────────────────────
// 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,
});