0; } function isAdmin(): bool{ return !empty($_SESSION['admin']); } function requireLogin(string $back = ''): void { if (!authed()) { flash('Please log in to continue.', 'warning'); go('/login.php?next='.urlencode($back ?: $_SERVER['REQUEST_URI'])); } } function requireAdmin(): void { if (!isAdmin()) { flash('Access denied.', 'error'); go('/index.php'); } } function loginUser(array $u): void { session_regenerate_id(true); $_SESSION['uid'] = $u['id']; $_SESSION['uname'] = $u['username']; $_SESSION['admin'] = (bool)$u['is_admin']; qrun("UPDATE users SET last_login=datetime('now') WHERE id=?", [$u['id']]); } function logoutUser(): void { session_unset(); session_destroy(); session_start(); session_regenerate_id(true); } /* ── Flash ────────────────────────────────────────────── */ function flash(string $msg, string $type = 'info'): void { $_SESSION['_flash'][] = [$type, $msg]; } function getFlashes(): array { $f = $_SESSION['_flash'] ?? []; unset($_SESSION['_flash']); return $f; } /* ── Output ───────────────────────────────────────────── */ function e(mixed $v): string { return htmlspecialchars((string)($v ?? ''), ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8'); } function go(string $url): never { header('Location:'.$url); exit; } /* ── Input ────────────────────────────────────────────── */ function p(string $k, mixed $d = ''): mixed { return $_POST[$k] ?? $d; } function g(string $k, mixed $d = ''): mixed { return $_GET[$k] ?? $d; } function iget(string $k): int { return (int)($_POST[$k] ?? $_GET[$k] ?? 0); } function ps(string $k): string { return trim((string)($_POST[$k] ?? '')); } function gs(string $k): string { return trim((string)($_GET[$k] ?? '')); } function isPost(): bool { return strtoupper($_SERVER['REQUEST_METHOD']) === 'POST'; } /* ── CSRF ─────────────────────────────────────────────── */ function csrf(): string { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(24)); return $_SESSION['csrf']; } function csrfField(): string { return ''; } function csrfCheck(): void { if (!hash_equals(csrf(), p('_csrf', ''))) { http_response_code(403); die('Invalid CSRF token.'); } } /* ── Settings ─────────────────────────────────────────── */ function setting(string $k, string $def = ''): string { $v = qval("SELECT value FROM settings WHERE key=?", [$k]); return ($v !== false && $v !== null) ? (string)$v : $def; } function setSetting(string $k, string $v): void { qrun("INSERT INTO settings(key,value)VALUES(?,?)ON CONFLICT(key)DO UPDATE SET value=excluded.value", [$k,$v]); } /* ── Ownership ────────────────────────────────────────── */ function owns(int $bizId): bool { if (isAdmin()) return true; if (!authed()) return false; return (bool)qval("SELECT 1 FROM business_owners WHERE user_id=? AND business_id=?", [uid(),$bizId]); } /* ── Stars ────────────────────────────────────────────── */ function starDisplay(float $avg): string { $r = (int)round($avg); $out = ''; for ($i = 1; $i <= 5; $i++) $out .= ''; return $out.''; } function starPicker(string $name = 'rating', int $sel = 0): string { $out = '
'; for ($i = 5; $i >= 1; $i--) { $chk = $sel === $i ? 'checked' : ''; $out .= ''; $out .= ''; } return $out.'
'; } /* ── Date / Money ─────────────────────────────────────── */ function ago(mixed $dt): string { if ($dt === null || $dt === '' || $dt === false) return ''; $ts = is_int($dt) ? $dt : @strtotime((string)$dt); if (!$ts) return ''; $d = time() - $ts; if ($d < 60) return 'Just now'; if ($d < 3600) return floor($d / 60).'m ago'; if ($d < 86400) return floor($d / 3600).'h ago'; if ($d < 604800) return floor($d / 86400).'d ago'; return date('M j, Y', $ts); } function fdate(mixed $dt, string $fmt = 'M j, Y'): string { if ($dt === null || $dt === '' || $dt === false) return ''; $ts = is_int($dt) ? $dt : @strtotime((string)$dt); return $ts ? date($fmt, $ts) : ''; } function money(float $n): string { return '$'.number_format($n, 2); } /* ── Misc ─────────────────────────────────────────────── */ function alertIcon(mixed $t): string { return match((string)($t ?? '')) { 'news' => '📰', 'event' => '🎉', 'warning' => '⚠️', 'sale' => '🏷️', default => 'ℹ️', }; } function parseHours(mixed $json): array { $s = (string)($json ?? ''); if ($s === '' || $s === '[]' || $s === 'null') return []; $decoded = json_decode($s, true); if (!is_array($decoded)) return []; // Must be an object (associative), not a plain list foreach (array_keys($decoded) as $key) { if (!is_string($key)) return []; } return $decoded; } function makeSlug(string $s): string { return trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($s)), '-'); } function uniqueSlug(string $base): string { $slug = makeSlug($base); $i = 0; while (qval("SELECT id FROM businesses WHERE slug=?", [$slug.($i ? "-$i" : "")])) $i++; return $slug . ($i ? "-$i" : ""); } function businessVideoProvider(string $url): string { $host = strtolower((string)(parse_url(trim($url), PHP_URL_HOST) ?? '')); $host = preg_replace('/^www\./', '', $host); if ($host === 'youtu.be' || $host === 'youtube.com' || str_ends_with($host, '.youtube.com') || $host === 'youtube-nocookie.com' || str_ends_with($host, '.youtube-nocookie.com')) return 'YouTube'; if ($host === 'vimeo.com' || str_ends_with($host, '.vimeo.com')) return 'Vimeo'; return ''; } function validBusinessVideoUrl(string $url): bool { $url = trim($url); if ($url === '') return true; $scheme = strtolower((string)(parse_url($url, PHP_URL_SCHEME) ?? '')); return in_array($scheme, ['http','https'], true) && businessVideoProvider($url) !== ''; }