diff --git a/admin/admin_header.php b/admin/admin_header.php index a848885..437539c 100644 --- a/admin/admin_header.php +++ b/admin/admin_header.php @@ -6,6 +6,7 @@ $pageTitle = ($adminTitle ?? 'Admin') . ' โ€” Keyser WV Admin'; include dirname(__DIR__).'/includes/header.php'; // Pending owner edit count for badge $_pendingEdits = (int)qval("SELECT COUNT(*) FROM business_edit_requests WHERE status='pending'"); +$_pendingClaims = (int)qval("SELECT COUNT(*) FROM business_claim_requests WHERE status='pending'"); ?>
+
+ + +
Optional. Business listing videos are rendered through lone-embed.php and should use YouTube or Vimeo.
+
+
diff --git a/admin/index.php b/admin/index.php index 076b439..2c207de 100644 --- a/admin/index.php +++ b/admin/index.php @@ -7,6 +7,7 @@ $stats = [ 'Reviews' => qval("SELECT COUNT(*) FROM reviews"), 'Events' => qval("SELECT COUNT(*) FROM events WHERE status='approved'"), 'Pending Events' => qval("SELECT COUNT(*) FROM events WHERE status='pending'"), + 'Pending Claims' => qval("SELECT COUNT(*) FROM business_claim_requests WHERE status='pending'"), 'Pending Edits' => qval("SELECT COUNT(*) FROM business_edit_requests WHERE status='pending'"), 'Open Reports' => qval("SELECT COUNT(*) FROM reports WHERE status='open'"), 'Active Alerts' => qval("SELECT COUNT(*) FROM alerts WHERE is_active=1"), @@ -15,6 +16,15 @@ $recentBiz = qall("SELECT * FROM businesses ORDER BY created_at DESC LIMIT 5" $recentUsers = qall("SELECT * FROM users ORDER BY created_at DESC LIMIT 5"); $pendingEvts = qall("SELECT e.*,u.username FROM events e LEFT JOIN users u ON u.id=e.submitted_by WHERE e.status='pending' ORDER BY e.created_at DESC LIMIT 5"); $openReports = qall("SELECT r.*,b.name biz,u.username reporter FROM reports r JOIN businesses b ON b.id=r.business_id JOIN users u ON u.id=r.user_id WHERE r.status='open' ORDER BY r.created_at DESC LIMIT 5"); +$pendingClaims = qall(" + SELECT bcr.*, b.name biz_name, b.slug biz_slug, u.username, u.email + FROM business_claim_requests bcr + JOIN businesses b ON b.id = bcr.business_id + JOIN users u ON u.id = bcr.user_id + WHERE bcr.status = 'pending' + ORDER BY bcr.created_at DESC + LIMIT 10 +"); $pendingEdits = qall(" SELECT ber.*, b.name biz_name, b.slug biz_slug, b.id biz_id, u.username FROM business_edit_requests ber @@ -30,7 +40,7 @@ $pendingEdits = qall("
$val): ?>
-
+
@@ -38,6 +48,34 @@ $pendingEdits = qall("
+ + +
+
+ ๐Ÿ”‘ BUSINESS PAGE CLAIM REQUESTS () +
+
+ + + + + + + + + + + + + +
BUSINESSREQUESTED BYCONTACT PHONESUBMITTED
+
+
+
Verify
+
+
+ +
@@ -50,7 +88,7 @@ $pendingEdits = qall(" 'Description','address'=>'Address','phone'=>'Phone', - 'email'=>'Email','hours'=>'Hours','is_active'=>'Status']; + 'email'=>'Email','website'=>'Website','video_url'=>'Video','hours'=>'Hours','is_active'=>'Status']; foreach ($pendingEdits as $pe): $preview = $pe['field'] === 'hours' ? '(hours update)' diff --git a/admin/owners.php b/admin/owners.php index 947983f..91d0b48 100644 --- a/admin/owners.php +++ b/admin/owners.php @@ -16,6 +16,23 @@ if (isPost()) { qrun("DELETE FROM business_owners WHERE user_id=? AND business_id=?",[(int)p('user_id'),(int)p('business_id')]); flash('Owner removed.','success'); } + if ($act === 'approve_claim') { + $claimId = (int)p('claim_id'); + $claim = qone("SELECT * FROM business_claim_requests WHERE id=? AND status='pending'", [$claimId]); + if ($claim) { + qrun("INSERT OR IGNORE INTO business_owners(user_id,business_id)VALUES(?,?)", [(int)$claim['user_id'], (int)$claim['business_id']]); + qrun("UPDATE business_claim_requests SET status='approved', reviewed_at=datetime('now') WHERE id=?", [$claimId]); + flash('Business claim approved and owner assigned.', 'success'); + } + go('/admin/owners.php#claims'); + } + if ($act === 'reject_claim') { + $claimId = (int)p('claim_id'); + qrun("UPDATE business_claim_requests SET status='rejected', admin_notes=?, reviewed_at=datetime('now') WHERE id=? AND status='pending'", + [ps('admin_notes'), $claimId]); + flash('Business claim rejected.', 'info'); + go('/admin/owners.php#claims'); + } go('/admin/owners.php'.($_GET?'?'.http_build_query($_GET):'')); } @@ -24,9 +41,66 @@ $filterBiz = iget('business_id'); $users = qall("SELECT id,username FROM users WHERE is_active=1 ORDER BY username"); $bizs = qall("SELECT id,name FROM businesses WHERE is_active=1 ORDER BY name"); $owners = qall("SELECT bo.*,u.username,b.name biz_name FROM business_owners bo JOIN users u ON u.id=bo.user_id JOIN businesses b ON b.id=bo.business_id ORDER BY b.name,u.username"); +$claims = qall(" + SELECT bcr.*, u.username, u.email, b.name biz_name, b.slug biz_slug + FROM business_claim_requests bcr + JOIN users u ON u.id=bcr.user_id + JOIN businesses b ON b.id=bcr.business_id + WHERE bcr.status='pending' + ORDER BY bcr.created_at DESC +"); ?> +
PENDING BUSINESS CLAIMS ()
+ +
+ + + + + + + + + + + + + + + +
BUSINESSREQUESTED BYCONTACT PHONEPENDING EDITSSUBMITTEDACTIONS
+
+
+
+ + edit + + None + + +
+
+ + + + +
+
+ + + + + +
+
+
+
+ +
๐Ÿ”‘

No pending business claims

+ +
๐Ÿ”‘ ASSIGN OWNER TO BUSINESS
diff --git a/admin/settings.php b/admin/settings.php index 6197ab1..deab8a0 100644 --- a/admin/settings.php +++ b/admin/settings.php @@ -12,6 +12,7 @@ if (isPost()) { setSetting('contact_email', ps('contact_email')); setSetting('registration_enabled',p('registration_enabled','0')); setSetting('events_require_approval',p('events_require_approval','0')); + setSetting('home_random_interval',p('home_random_interval','2hours') === 'day' ? 'day' : '2hours'); flash('Settings saved!','success'); go('/admin/settings.php'); } @@ -33,6 +34,7 @@ $evtApproval = setting('events_require_approval','1'); $siteName = setting('site_name','Discover Keyser WV'); $siteTagline = setting('site_tagline','The Friendliest City in the U.S.A.'); $contactEmail = setting('contact_email','info@discoverkeyser.com'); +$homeRandomInterval = setting('home_random_interval','2hours'); $me = qone("SELECT * FROM users WHERE id=?",[uid()]); ?> @@ -79,6 +81,20 @@ $me = qone("SELECT * FROM users WHERE id=?",[uid()]);
+
+ +
+ + +
+
+
diff --git a/assets/css/style.css b/assets/css/style.css index d779f50..08e858d 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,21 +1,23 @@ /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - DISCOVER KEYSER WV โ€” Dark West Virginia Theme - WV Gold #c9a84c ยท Navy #1a3a5c ยท Near-black backgrounds + DISCOVER KEYSER WV โ€” Neon Mountain Night Theme + WV gold, electric cyan, aurora magenta, deep black backgrounds โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ :root { - --gold:#c9a84c; --gold-l:#e2c06d; --gold-d:#8a6f2e; --gold-dim:rgba(201,168,76,.13); - --navy:#1a3a5c; --navy-m:#1e4876; --navy-l:#2a5f99; - --bg0:#07090e; --bg1:#0a0f18; --bg2:#0d1520; --bg3:#111c2c; --bg4:#162436; --bg5:#1c2e46; - --bdr:#1e3050; --bdr-l:#253d60; - --text:#e2ddd0; --text-m:#9aa4b8; --text-d:#58687e; - --green:#2a7a47; --green-l:#3da558; --red:#8f2d2d; --red-l:#c04040; --amber:#b37820; - --r:6px; --r-lg:12px; - --sh:0 6px 30px rgba(0,0,0,.55); --sh-sm:0 2px 12px rgba(0,0,0,.4); + --gold:#ffd166; --gold-l:#ffe08f; --gold-d:#a57824; --gold-dim:rgba(255,209,102,.14); + --cyan:#26f4ff; --cyan-l:#8ffbff; --cyan-d:#118895; --cyan-dim:rgba(38,244,255,.13); + --pink:#ff4fd8; --pink-l:#ff92ea; --pink-d:#9c2d82; --pink-dim:rgba(255,79,216,.12); + --navy:#142142; --navy-m:#1f3769; --navy-l:#3157a5; + --bg0:#03050b; --bg1:#060914; --bg2:#0a1020; --bg3:#0e172b; --bg4:#121f38; --bg5:#172846; + --bdr:#1b3555; --bdr-l:#285076; + --text:#f2f4ff; --text-m:#aab7cf; --text-d:#65768f; + --green:#1d8f62; --green-l:#4dff9b; --red:#9d2c4a; --red-l:#ff5f7b; --amber:#e09a2a; + --r:6px; --r-lg:8px; + --sh:0 12px 42px rgba(0,0,0,.62),0 0 34px rgba(38,244,255,.06); --sh-sm:0 4px 18px rgba(0,0,0,.45); --t:.18s ease; } *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} html{scroll-behavior:smooth} -body{font-family:'Source Sans 3',sans-serif;background:var(--bg1);color:var(--text);line-height:1.65;min-height:100vh;-webkit-font-smoothing:antialiased} +body{font-family:'Source Sans 3',sans-serif;background:radial-gradient(circle at 12% 0%,rgba(38,244,255,.12),transparent 28rem),radial-gradient(circle at 88% 8%,rgba(255,79,216,.1),transparent 30rem),linear-gradient(180deg,var(--bg0),var(--bg1) 28rem,var(--bg2));color:var(--text);line-height:1.65;min-height:100vh;-webkit-font-smoothing:antialiased} a{color:var(--gold);text-decoration:none;transition:color var(--t)} a:hover{color:var(--gold-l)} h1,h2,h3{font-family:'Playfair Display',serif;line-height:1.15} @@ -25,19 +27,19 @@ button{cursor:pointer;font-family:inherit} input,select,textarea{font-family:inh address{font-style:normal} /* โ”€โ”€ Navbar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.navbar{position:sticky;top:0;z-index:1000;background:rgba(7,9,14,.97);border-bottom:1px solid var(--gold-dim);backdrop-filter:blur(16px)} +.navbar{position:sticky;top:0;z-index:1000;background:rgba(3,5,11,.86);border-bottom:1px solid rgba(38,244,255,.18);backdrop-filter:blur(18px);box-shadow:0 1px 0 rgba(255,209,102,.08),0 0 28px rgba(38,244,255,.05)} .nav-wrap{max-width:1440px;margin:0 auto;padding:0 1.5rem;height:64px;display:flex;align-items:center;justify-content:space-between;gap:1rem} .nav-brand{display:flex;align-items:center;gap:.65rem;color:var(--text);flex-shrink:0} -.brand-mtn{font-size:1.9rem;filter:drop-shadow(0 0 10px rgba(201,168,76,.45))} +.brand-mtn{font-size:1.9rem;filter:drop-shadow(0 0 10px rgba(38,244,255,.55))} .nav-brand>div{display:flex;flex-direction:column;line-height:1} -.brand-city{font-family:'Oswald',sans-serif;font-size:1.15rem;font-weight:600;letter-spacing:.2em;color:var(--gold)} +.brand-city{font-family:'Oswald',sans-serif;font-size:1.15rem;font-weight:600;letter-spacing:.2em;color:var(--gold);text-shadow:0 0 16px rgba(255,209,102,.24)} .brand-wv{font-size:.52rem;letter-spacing:.3em;color:var(--text-m)} .nav-links{display:flex;align-items:center;gap:.12rem;flex-wrap:nowrap} .nl{display:block;padding:.38rem .72rem;border-radius:4px;font-family:'Oswald',sans-serif;font-size:.78rem;letter-spacing:.1em;color:var(--text-m);transition:all var(--t)} -.nl:hover,.nl.active{color:var(--gold);background:var(--gold-dim)} +.nl:hover,.nl.active{color:var(--cyan-l);background:var(--cyan-dim)} .nl-admin{color:var(--gold)!important;border:1px solid var(--gold-dim)} -.nl-join{background:var(--gold);color:var(--bg0)!important;font-weight:600;padding:.38rem .85rem} -.nl-join:hover{background:var(--gold-l);color:var(--bg0)!important} +.nl-join{background:linear-gradient(135deg,var(--gold),var(--cyan));color:var(--bg0)!important;font-weight:600;padding:.38rem .85rem;box-shadow:0 0 18px rgba(38,244,255,.18)} +.nl-join:hover{background:linear-gradient(135deg,var(--gold-l),var(--cyan-l));color:var(--bg0)!important} .user-wrap{position:relative} .user-btn{background:var(--bg4);border:1px solid var(--bdr-l);color:var(--text-m);padding:.38rem .75rem;border-radius:4px;font-family:'Oswald',sans-serif;font-size:.78rem;letter-spacing:.08em;transition:all var(--t)} .user-btn:hover{border-color:var(--gold-d);color:var(--gold)} @@ -60,7 +62,7 @@ address{font-style:normal} /* โ”€โ”€ Hero โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .hero{position:relative;min-height:90vh;display:flex;align-items:center;justify-content:center;overflow:hidden;background:var(--bg0)} -.hero-bg{position:absolute;inset:0;background:radial-gradient(ellipse 75% 50% at 50% 30%,rgba(26,58,92,.58),transparent 70%),radial-gradient(ellipse 45% 35% at 15% 80%,rgba(201,168,76,.06),transparent),linear-gradient(180deg,var(--bg0),#0c1828 55%,#060a0f)} +.hero-bg{position:absolute;inset:0;background:radial-gradient(ellipse 75% 50% at 50% 30%,rgba(38,244,255,.25),transparent 70%),radial-gradient(ellipse 45% 35% at 15% 80%,rgba(255,209,102,.08),transparent),linear-gradient(180deg,var(--bg0),#071526 55%,#03050b)} .hero-stars{position:absolute;inset:0;pointer-events:none;background:radial-gradient(1px 1px at 8% 10%,rgba(255,255,255,.6),transparent),radial-gradient(2px 2px at 38% 18%,rgba(201,168,76,.55),transparent),radial-gradient(1px 1px at 55% 8%,rgba(255,255,255,.45),transparent),radial-gradient(1px 1px at 72% 22%,rgba(255,255,255,.35),transparent),radial-gradient(1px 1px at 87% 14%,rgba(255,255,255,.5),transparent),radial-gradient(1px 1px at 20% 45%,rgba(255,255,255,.2),transparent),radial-gradient(1px 1px at 62% 42%,rgba(255,255,255,.28),transparent)} .hero-mtns{position:absolute;bottom:0;left:0;right:0;height:48%;z-index:1} .hero-mtns svg{width:100%;height:100%} @@ -75,12 +77,12 @@ address{font-style:normal} /* โ”€โ”€ Buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .btn{display:inline-flex;align-items:center;gap:.4rem;padding:.72rem 1.65rem;border-radius:var(--r);font-family:'Oswald',sans-serif;font-size:.82rem;letter-spacing:.12em;font-weight:500;border:none;cursor:pointer;transition:all var(--t);text-decoration:none;white-space:nowrap} -.btn-primary{background:var(--gold);color:var(--bg0)} -.btn-primary:hover{background:var(--gold-l);color:var(--bg0);transform:translateY(-1px);box-shadow:0 4px 20px rgba(201,168,76,.3)} -.btn-outline{background:transparent;border:1px solid var(--gold-d);color:var(--gold)} -.btn-outline:hover{background:var(--gold-dim);border-color:var(--gold)} +.btn-primary{background:linear-gradient(135deg,var(--gold),var(--cyan));color:var(--bg0);box-shadow:0 0 20px rgba(38,244,255,.16)} +.btn-primary:hover{background:linear-gradient(135deg,var(--gold-l),var(--cyan-l));color:var(--bg0);transform:translateY(-1px);box-shadow:0 6px 26px rgba(38,244,255,.22)} +.btn-outline{background:rgba(38,244,255,.03);border:1px solid rgba(38,244,255,.42);color:var(--cyan-l)} +.btn-outline:hover{background:var(--cyan-dim);border-color:var(--cyan);color:var(--text)} .btn-secondary{background:var(--bg4);border:1px solid var(--bdr-l);color:var(--text-m)} -.btn-secondary:hover{background:var(--bg5);border-color:var(--gold-d);color:var(--text)} +.btn-secondary:hover{background:var(--bg5);border-color:var(--pink-d);color:var(--text)} .btn-navy{background:var(--navy-m);color:#fff} .btn-navy:hover{background:var(--navy-l)} .btn-danger{background:var(--red);color:#fff} @@ -92,7 +94,7 @@ address{font-style:normal} .btn-block{width:100%;justify-content:center} /* โ”€โ”€ Stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.stats-strip{background:var(--bg2);border-top:1px solid var(--gold-dim);border-bottom:1px solid var(--gold-dim);padding:2rem 1.5rem} +.stats-strip{background:rgba(10,16,32,.78);border-top:1px solid var(--cyan-dim);border-bottom:1px solid var(--gold-dim);padding:2rem 1.5rem} .stats-inner{max-width:1440px;margin:0 auto;display:flex;justify-content:space-around;flex-wrap:wrap;gap:.5rem} .stat{text-align:center;padding:.75rem 1.25rem} .stat-n{font-family:'Playfair Display',serif;font-size:2.6rem;font-weight:900;color:var(--gold);line-height:1} @@ -106,6 +108,50 @@ address{font-style:normal} .eyebrow{font-family:'Oswald',sans-serif;font-size:.68rem;letter-spacing:.32em;color:var(--gold);margin-bottom:.5rem} .sec-title{font-size:clamp(1.8rem,3vw,2.6rem)} .sec-rule{width:44px;height:2px;background:var(--gold);margin-top:.7rem} +.section-head{display:flex;align-items:end;justify-content:space-between;gap:1rem;margin-bottom:2rem} +.card-link{display:block;text-decoration:none;height:100%} +.view-label{color:var(--cyan-l);font-size:.74rem;font-family:'Oswald',sans-serif;letter-spacing:.14em} + +/* โ”€โ”€ Neon homepage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.home-hero{position:relative;overflow:hidden;min-height:calc(100vh - 64px);display:flex;align-items:center;padding:5.5rem 1.5rem 4.5rem;background:linear-gradient(145deg,rgba(3,5,11,.9),rgba(10,16,32,.98))} +.home-hero::before{content:"";position:absolute;inset:0;background:linear-gradient(rgba(38,244,255,.055) 1px,transparent 1px),linear-gradient(90deg,rgba(38,244,255,.045) 1px,transparent 1px);background-size:44px 44px;mask-image:linear-gradient(180deg,rgba(0,0,0,.9),transparent 86%);pointer-events:none} +.home-hero-glow{position:absolute;inset:0;background:radial-gradient(circle at 18% 30%,rgba(38,244,255,.22),transparent 24rem),radial-gradient(circle at 78% 20%,rgba(255,79,216,.16),transparent 26rem),radial-gradient(circle at 54% 86%,rgba(255,209,102,.1),transparent 30rem)} +.home-hero-grid{position:relative;z-index:1;max-width:1440px;margin:0 auto;width:100%;display:grid;grid-template-columns:minmax(0,1.12fr) minmax(320px,.72fr);gap:3rem;align-items:center} +.home-hero-copy{text-align:left;max-width:820px} +.home-hero .hero-eyebrow{border-color:rgba(38,244,255,.32);background:rgba(38,244,255,.08);color:var(--cyan-l)} +.home-hero .hero-h1{text-align:left;font-size:clamp(3.5rem,9vw,7.4rem);text-shadow:0 0 34px rgba(38,244,255,.2),0 8px 54px rgba(0,0,0,.85)} +.home-hero .hero-h1 .gld{background:linear-gradient(90deg,var(--gold),var(--cyan),var(--pink-l));-webkit-background-clip:text;background-clip:text;color:transparent} +.home-hero .hero-desc{margin:1.2rem 0 0;max-width:650px;color:var(--text-m);font-size:1.12rem} +.hero-search{margin-top:2rem;display:flex;gap:.65rem;max-width:670px;position:relative} +.hero-search .s-icon{left:1rem;color:var(--cyan)} +.hero-search .search-input{padding-left:2.6rem;border-color:rgba(38,244,255,.34);background:rgba(0,0,0,.42)} +.home-hero .hero-actions{justify-content:flex-start;margin-top:1.25rem} +.hero-signal{display:grid;gap:1rem} +.signal-card{background:linear-gradient(180deg,rgba(18,31,56,.82),rgba(6,9,20,.86));border:1px solid rgba(38,244,255,.18);border-radius:var(--r);padding:1.25rem;box-shadow:0 0 28px rgba(38,244,255,.07)} +.signal-card-main{min-height:210px;display:flex;flex-direction:column;justify-content:center;border-color:rgba(255,209,102,.26)} +.signal-label,.signal-copy{font-family:'Oswald',sans-serif;letter-spacing:.14em;font-size:.68rem;color:var(--text-d);text-transform:uppercase} +.signal-number{font-family:'Playfair Display',serif;font-weight:900;font-size:clamp(4rem,8vw,6.5rem);line-height:.95;color:var(--gold);text-shadow:0 0 28px rgba(255,209,102,.2)} +.signal-small{font-family:'Playfair Display',serif;font-weight:900;font-size:1.55rem;color:var(--cyan-l);line-height:1.05} +.signal-mini-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem} +.neon-alerts{border-top:1px solid rgba(38,244,255,.14);background:rgba(6,9,20,.78)} +.home-band{background:linear-gradient(180deg,rgba(10,16,32,.92),rgba(6,9,20,.92));border-top:1px solid rgba(255,79,216,.13);border-bottom:1px solid rgba(38,244,255,.13)} +.event-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem} +.event-strip .evt-card{height:100%;margin-bottom:0} +.event-empty{padding:2rem;border:1px dashed rgba(38,244,255,.22);border-radius:var(--r);background:rgba(0,0,0,.14)} +.about-split{display:grid;grid-template-columns:minmax(0,.95fr) minmax(320px,1fr);gap:3rem;align-items:start} +.home-facts{grid-template-columns:repeat(2,minmax(0,1fr))} +.random-section{padding-top:3rem} +.random-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem} +.random-row{display:grid;grid-template-columns:44px 1fr auto;align-items:center;gap:.8rem;padding:.9rem 1rem;background:rgba(18,31,56,.78);border:1px solid rgba(38,244,255,.14);border-radius:var(--r);color:var(--text);transition:all var(--t)} +.random-row:hover{border-color:rgba(38,244,255,.42);background:rgba(18,31,56,.96);transform:translateY(-1px);box-shadow:var(--sh-sm)} +.random-cat{display:grid;place-items:center;width:36px;height:36px;border-radius:50%;background:rgba(38,244,255,.1);border:1px solid rgba(38,244,255,.2)} +.random-main{min-width:0} +.random-main strong{display:block;color:var(--text);font-size:.96rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.random-main em{display:block;color:var(--text-d);font-style:normal;font-size:.78rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.random-arrow{color:var(--cyan);font-family:'Oswald',sans-serif} +.join-band{max-width:1200px;margin:0 auto 4rem;padding:2rem 1.5rem;display:flex;align-items:center;justify-content:space-between;gap:1.5rem;border-top:1px solid rgba(38,244,255,.18);border-bottom:1px solid rgba(255,209,102,.18)} +.join-band h2{font-size:clamp(1.6rem,3vw,2.3rem);margin:.2rem 0 .45rem} +.join-band p{max-width:720px;color:var(--text-m);line-height:1.75} /* โ”€โ”€ Grids โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .g3{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1.4rem} @@ -113,19 +159,19 @@ address{font-style:normal} .g2{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:1.5rem} /* โ”€โ”€ Business card โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -.biz-card{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column;transition:all .22s;height:100%} -.biz-card:hover{transform:translateY(-3px);border-color:var(--gold-d);box-shadow:var(--sh)} -.biz-card-top{padding:1.2rem 1.4rem;background:linear-gradient(135deg,var(--bg4),var(--bg3));border-bottom:1px solid var(--bdr)} -.cat-badge{display:inline-block;padding:.16rem .52rem;border-radius:2px;font-family:'Oswald',sans-serif;font-size:.6rem;letter-spacing:.14em;background:var(--gold-dim);border:1px solid rgba(201,168,76,.25);color:var(--gold);margin-bottom:.45rem} +.biz-card{background:linear-gradient(180deg,rgba(18,31,56,.96),rgba(9,15,30,.96));border:1px solid rgba(38,244,255,.16);border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column;transition:all .22s;height:100%;box-shadow:0 0 0 1px rgba(255,209,102,.03)} +.biz-card:hover{transform:translateY(-3px);border-color:rgba(38,244,255,.48);box-shadow:var(--sh)} +.biz-card-top{padding:1.2rem 1.4rem;background:linear-gradient(135deg,rgba(38,244,255,.1),rgba(255,79,216,.06),rgba(255,209,102,.05));border-bottom:1px solid rgba(38,244,255,.13)} +.cat-badge{display:inline-block;padding:.16rem .52rem;border-radius:2px;font-family:'Oswald',sans-serif;font-size:.6rem;letter-spacing:.14em;background:var(--cyan-dim);border:1px solid rgba(38,244,255,.28);color:var(--cyan-l);margin-bottom:.45rem} .biz-name{font-size:1.12rem;font-weight:700;color:var(--text);line-height:1.2} .biz-sub{font-size:.78rem;color:var(--text-d);margin-top:.15rem} .biz-card-body{padding:1rem 1.4rem;flex:1} .biz-desc{font-size:.86rem;color:var(--text-m);line-height:1.65;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} .biz-meta{margin-top:.8rem;display:flex;flex-direction:column;gap:.3rem} .biz-meta-row{display:flex;gap:.45rem;font-size:.78rem;color:var(--text-d);align-items:flex-start} -.biz-meta-icon{color:var(--gold);flex-shrink:0;width:16px;text-align:center} -.biz-card-foot{padding:.9rem 1.4rem;border-top:1px solid var(--bdr);background:var(--bg4);display:flex;justify-content:space-between;align-items:center} -.featured-badge{background:linear-gradient(135deg,var(--gold),#a07828);color:var(--bg0);font-family:'Oswald',sans-serif;font-size:.6rem;letter-spacing:.18em;padding:.15rem .5rem;border-radius:2px} +.biz-meta-icon{color:var(--cyan);flex-shrink:0;width:16px;text-align:center} +.biz-card-foot{padding:.9rem 1.4rem;border-top:1px solid rgba(38,244,255,.14);background:rgba(18,31,56,.72);display:flex;justify-content:space-between;align-items:center} +.featured-badge{background:linear-gradient(135deg,var(--gold),var(--pink));color:var(--bg0);font-family:'Oswald',sans-serif;font-size:.6rem;letter-spacing:.18em;padding:.15rem .5rem;border-radius:2px} /* โ”€โ”€ Stars โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .stars{display:inline-flex;gap:1px} @@ -209,6 +255,45 @@ address{font-style:normal} .fhint{font-size:.75rem;color:var(--text-d);margin-top:.3rem} .form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem} .err-box{padding:.65rem 1rem;border-radius:4px;margin-bottom:1rem;font-size:.88rem;background:rgba(143,45,45,.28);border:1px solid var(--red-l);color:#ff9a9a} +.hp-field{position:absolute;left:-9999px;top:-9999px;opacity:0;height:0;width:0;overflow:hidden;pointer-events:none} +.tos-check{display:flex;align-items:flex-start;gap:.65rem;background:rgba(18,31,56,.78);border:1px solid rgba(38,244,255,.18);border-radius:var(--r);padding:1rem 1.15rem;cursor:pointer;transition:border-color var(--t);margin:1.4rem 0} +.tos-check:has(input:checked){border-color:rgba(77,255,155,.5)} +.tos-check input[type="checkbox"]{flex-shrink:0;margin-top:3px;width:17px;height:17px;accent-color:var(--green-l);cursor:pointer} +.tos-check-text{font-size:.88rem;color:var(--text-m);line-height:1.6;user-select:none} +.tos-check-text strong{color:var(--text)} + +/* โ”€โ”€ Signup tiers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.signup-shell{max-width:1040px;margin:0 auto;padding:4rem 1.5rem} +.signup-intro{text-align:center;margin-bottom:2rem} +.signup-intro .eyebrow{justify-content:center;display:flex;color:var(--cyan-l)} +.signup-intro h1{font-size:clamp(2.3rem,5vw,3.5rem);margin:.3rem 0} +.signup-intro p{max-width:650px;margin:0 auto;color:var(--text-m);line-height:1.75} +.signup-errors{max-width:860px;margin:0 auto 1.25rem} +.signup-card{background:linear-gradient(180deg,rgba(14,23,43,.96),rgba(8,13,27,.96));border:1px solid rgba(38,244,255,.18);border-radius:var(--r-lg);padding:1.5rem;box-shadow:var(--sh);position:relative} +.tier-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1.5rem} +.tier-card{display:grid;gap:.35rem;align-content:start;min-height:170px;border:1px solid rgba(38,244,255,.16);border-radius:var(--r);padding:1.25rem;background:rgba(18,31,56,.7);cursor:pointer;transition:all var(--t)} +.tier-card:hover{border-color:rgba(38,244,255,.45);transform:translateY(-1px)} +.tier-card input{position:absolute;opacity:0;pointer-events:none} +.tier-card:has(input:checked){border-color:var(--gold);background:linear-gradient(180deg,rgba(255,209,102,.12),rgba(38,244,255,.08));box-shadow:0 0 24px rgba(255,209,102,.08)} +.tier-card-owner:has(input:checked){border-color:var(--cyan)} +.tier-kicker{font-family:'Oswald',sans-serif;font-size:.66rem;letter-spacing:.22em;color:var(--cyan-l);text-transform:uppercase} +.tier-title{font-family:'Playfair Display',serif;font-size:1.35rem;color:var(--text);line-height:1.15} +.tier-copy{font-size:.9rem;color:var(--text-m);line-height:1.55} +.signup-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem} +.signup-grid .fg{margin-bottom:0} +.signup-wide{grid-column:1/-1} +.owner-claim-fields{display:none;margin-top:1.5rem;padding-top:1.5rem;border-top:1px solid rgba(38,244,255,.15)} +.owner-claim-fields.is-open{display:block} +.owner-claim-note{background:rgba(38,244,255,.08);border:1px solid rgba(38,244,255,.22);border-radius:var(--r);padding:1rem 1.15rem;color:var(--text-m);font-size:.9rem;line-height:1.65;margin-bottom:1rem} +.owner-claim-note strong{color:var(--cyan-l)} +.claim-edit-panel{margin-top:1rem;background:rgba(0,0,0,.16);border:1px solid rgba(255,209,102,.14);border-radius:var(--r);padding:1.2rem} +.signup-submit{font-size:.9rem;padding:.85rem} +.signin-link{text-align:center;margin-top:1.1rem;font-size:.84rem;color:var(--text-d)} + +/* โ”€โ”€ Embedded business video โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.business-video-box{border-color:rgba(38,244,255,.24)} +.embed-shell{position:relative;width:100%;aspect-ratio:16/9;overflow:hidden;border-radius:var(--r);background:#000;border:1px solid rgba(38,244,255,.22);box-shadow:0 0 26px rgba(38,244,255,.08)} +.embed-shell iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#000} /* โ”€โ”€ Search โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .search-bar{background:var(--bg2);padding:1.75rem 1.5rem;border-bottom:1px solid var(--bdr)} @@ -324,13 +409,14 @@ address{font-style:normal} @keyframes fadeIn{from{opacity:0}to{opacity:1}} /* โ”€โ”€ Responsive โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ -@media(max-width:1100px){.footer-grid{grid-template-columns:1fr 1fr}} -@media(max-width:900px){.detail-body{grid-template-columns:1fr}.admin-wrap{grid-template-columns:1fr}.admin-side{position:static;max-height:none;border-right:none;border-bottom:1px solid var(--bdr)}.form-row{grid-template-columns:1fr}} +@media(max-width:1100px){.footer-grid{grid-template-columns:1fr 1fr}.home-hero-grid{grid-template-columns:1fr}.hero-signal{grid-template-columns:1fr 1fr}.signal-card-main{min-height:auto}.about-split{grid-template-columns:1fr}} +@media(max-width:900px){.detail-body{grid-template-columns:1fr}.admin-wrap{grid-template-columns:1fr}.admin-side{position:static;max-height:none;border-right:none;border-bottom:1px solid var(--bdr)}.form-row{grid-template-columns:1fr}.section-head{align-items:flex-start;flex-direction:column}.random-list{grid-template-columns:1fr}.signup-grid,.tier-grid{grid-template-columns:1fr}.home-facts{grid-template-columns:1fr}} @media(max-width:768px){ .nav-links{display:none;position:absolute;top:64px;left:0;right:0;background:var(--bg0);border-bottom:1px solid var(--bdr-l);flex-direction:column;padding:.75rem 1rem;gap:.1rem;box-shadow:var(--sh)} .nav-links.open{display:flex}.hamburger{display:flex} .user-wrap{width:100%}.user-btn{width:100%;text-align:left} .user-dd{position:static;box-shadow:none;border:none;background:var(--bg2)} .hero{min-height:80vh}.g3,.g4{grid-template-columns:1fr}.evt-card{flex-direction:column} + .home-hero{min-height:auto;padding:4rem 1rem 3rem}.home-hero .hero-h1{font-size:clamp(3rem,17vw,4.6rem)}.hero-search{flex-direction:column}.hero-search .s-icon{top:1.35rem}.hero-search .btn{justify-content:center}.hero-signal,.signal-mini-grid{grid-template-columns:1fr}.join-band{align-items:flex-start;flex-direction:column;margin-bottom:2.5rem}.signup-shell{padding:3rem 1rem}.signup-card{padding:1rem}.tier-card{min-height:auto} } -@media(max-width:560px){.hero-h1{font-size:3rem}.footer-grid{grid-template-columns:1fr}.hrs-grid{grid-template-columns:1fr}.form-box{margin:2rem 1rem}} +@media(max-width:560px){.hero-h1{font-size:3rem}.footer-grid{grid-template-columns:1fr}.hrs-grid{grid-template-columns:1fr}.form-box{margin:2rem 1rem}.random-row{grid-template-columns:36px 1fr}.random-arrow{display:none}.home-hero .hero-eyebrow{letter-spacing:.18em}.btn{white-space:normal;text-align:center}.signal-number{font-size:3.6rem}} diff --git a/business.php b/business.php index 484acba..5ebeee7 100644 --- a/business.php +++ b/business.php @@ -5,7 +5,7 @@ $slug = gs('slug'); $b = qone(" SELECT b.id, b.name, b.slug, b.subcategory, b.description, - b.address, b.phone, b.email, b.website, + b.address, b.phone, b.email, b.website, COALESCE(b.video_url,'') AS video_url, COALESCE(b.hours,'{}') AS hours, b.lat, b.lng, b.is_active, b.is_featured, b.created_at, b.category_id, @@ -30,7 +30,7 @@ if (isPost()) { // Owner submits edit request if ($act === 'owner_edit' && owns($b['id']) && !isAdmin()) { $days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; - $allowedFields = ['description','address','phone','email','is_active']; + $allowedFields = ['description','address','phone','email','website','video_url','is_active']; $submitted = 0; // Simple text fields @@ -42,6 +42,10 @@ if (isPost()) { $new = (string)(int)p('is_active', $b['is_active']); $old = (string)(int)$b['is_active']; } + if ($field === 'video_url' && !validBusinessVideoUrl($new)) { + flash('Business videos must be a YouTube or Vimeo URL.', 'error'); + go("/business.php?slug=$slug"); + } if ($new !== $old) { // Remove any existing pending request for this field qrun("DELETE FROM business_edit_requests WHERE business_id=? AND field=? AND status='pending'", @@ -299,6 +303,28 @@ include __DIR__.'/includes/header.php'; value="">
+
+
+ + +
+
+ + +
Optional. Business owners may use YouTube or Vimeo.
+
+
+
+ +
+
VIDEO FEATURE
+
+ +
+
+ +
๐Ÿฝ๏ธ MENU
diff --git a/dashboard.php b/dashboard.php index 6add144..fc18821 100644 --- a/dashboard.php +++ b/dashboard.php @@ -9,6 +9,14 @@ $myBiz = $bizIds ? qall("SELECT b.*,c.name cat,COALESCE(AVG(r.rating),0) avg,COUNT(r.id) rcnt FROM businesses b JOIN categories c ON c.id=b.category_id LEFT JOIN reviews r ON r.business_id=b.id WHERE b.id IN(".implode(',',array_fill(0,count($bizIds),'?')).") GROUP BY b.id ORDER BY b.name",$bizIds) : []; $myEvents = authed() ? qall("SELECT * FROM events WHERE submitted_by=? ORDER BY created_at DESC LIMIT 10",[uid()]) : []; +$myClaims = authed() ? qall(" + SELECT bcr.*, b.name biz_name, b.slug biz_slug + FROM business_claim_requests bcr + JOIN businesses b ON b.id=bcr.business_id + WHERE bcr.user_id=? + ORDER BY bcr.created_at DESC + LIMIT 5 +", [uid()]) : []; include __DIR__.'/includes/header.php'; ?>
@@ -19,6 +27,32 @@ include __DIR__.'/includes/header.php';
+ +
BUSINESS CLAIMS
+
+ + + + + + + + + + + + + +
BUSINESSCONTACT PHONESTATUSSUBMITTED
+ 'badge-green','pending'=>'badge-gold','rejected'=>'badge-red']; ?> + + +
We will contact you within a few business days for verification.
+ +
View Page
+
+ +
MY BUSINESS LISTINGS
diff --git a/includes/db.php b/includes/db.php index f8716ed..33aae35 100644 --- a/includes/db.php +++ b/includes/db.php @@ -18,8 +18,8 @@ function db(): PDO { ]); $pdo->exec("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA synchronous=NORMAL;"); if ($isNew) { _schema($pdo); _seed($pdo); } - // Always ensure the edit_requests table exists (for existing DBs) - _ensureEditRequests($pdo); + // Always ensure app upgrade tables/columns exist (for existing DBs) + _ensureAppUpgrades($pdo); if (file_exists(DB_FILE)) @chmod(DB_FILE, 0666); return $pdo; } @@ -31,8 +31,20 @@ function qone(string $sql, array $p=[]): ?array { $r=qr($sql,$p)->fetch(); retu function qval(string $sql, array $p=[]): mixed { return qr($sql,$p)->fetchColumn(); } function qrun(string $sql, array $p=[]): int { $s=db()->prepare($sql); $s->execute($p); return (int)db()->lastInsertId(); } -/* โ”€โ”€ Ensure edit requests table exists on old DBs โ”€โ”€โ”€ */ -function _ensureEditRequests(PDO $db): void { +/* โ”€โ”€ Ensure upgraded tables/columns exist on old DBs โ”€โ”€โ”€ */ +function _hasColumn(PDO $db, string $table, string $column): bool { + $cols = $db->query("PRAGMA table_info($table)")->fetchAll(PDO::FETCH_ASSOC); + foreach ($cols as $col) { + if (($col['name'] ?? '') === $column) return true; + } + return false; +} + +function _ensureAppUpgrades(PDO $db): void { + if (!_hasColumn($db, 'businesses', 'video_url')) { + $db->exec("ALTER TABLE businesses ADD COLUMN video_url TEXT NOT NULL DEFAULT ''"); + } + $db->exec(" CREATE TABLE IF NOT EXISTS business_edit_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -46,7 +58,24 @@ function _ensureEditRequests(PDO $db): void { created_at TEXT NOT NULL DEFAULT(datetime('now')), reviewed_at TEXT ); + CREATE TABLE IF NOT EXISTS business_claim_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + contact_phone TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + admin_notes TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT(datetime('now')), + reviewed_at TEXT + ); + CREATE INDEX IF NOT EXISTS idx_claim_requests_status ON business_claim_requests(status, created_at); "); + + $defaults = [ + 'home_random_interval' => '2hours', + ]; + $stmt = $db->prepare("INSERT OR IGNORE INTO settings(key,value)VALUES(?,?)"); + foreach ($defaults as $k => $v) $stmt->execute([$k, $v]); } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -84,6 +113,7 @@ function _schema(PDO $db): void { phone TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '', website TEXT NOT NULL DEFAULT '', + video_url TEXT NOT NULL DEFAULT '', hours TEXT NOT NULL DEFAULT '{}', lat REAL NOT NULL DEFAULT 39.4364, lng REAL NOT NULL DEFAULT -78.9762, @@ -108,6 +138,16 @@ function _schema(PDO $db): void { created_at TEXT NOT NULL DEFAULT(datetime('now')), reviewed_at TEXT ); + CREATE TABLE IF NOT EXISTS business_claim_requests( + id INTEGER PRIMARY KEY AUTOINCREMENT, + business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + contact_phone TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + admin_notes TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT(datetime('now')), + reviewed_at TEXT + ); CREATE TABLE IF NOT EXISTS reviews( id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, @@ -198,7 +238,8 @@ function _seed(PDO $db): void { foreach (['registration_enabled'=>'1','site_name'=>'Discover Keyser WV', 'site_tagline'=>'The Friendliest City in the U.S.A.', 'contact_email'=>'info@discoverkeyser.com', - 'events_require_approval'=>'1'] as $k=>$v) + 'events_require_approval'=>'1', + 'home_random_interval'=>'2hours'] as $k=>$v) qrun("INSERT OR IGNORE INTO settings(key,value)VALUES(?,?)",[$k,$v]); /* Admin user โ€” password "password123" */ diff --git a/includes/functions.php b/includes/functions.php index 342d92e..61369ca 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -135,3 +135,17 @@ function uniqueSlug(string $base): string { 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) !== ''; +} diff --git a/index.php b/index.php index 68d47ba..42acfe4 100644 --- a/index.php +++ b/index.php @@ -2,59 +2,79 @@ require_once __DIR__.'/includes/boot.php'; $pageTitle='Discover Keyser WV โ€” The Friendliest City in the U.S.A.'; $activeNav='home'; -$alerts=qall("SELECT a.*,b.name bname,b.id bid,b.slug bslug FROM alerts a JOIN businesses b ON b.id=a.business_id WHERE a.is_active=1 ORDER BY a.created_at DESC LIMIT 5"); -$featured=qall("SELECT b.*,c.name cat,c.icon cat_icon,COALESCE(AVG(r.rating),0) avg,COUNT(r.id) rcnt FROM businesses b JOIN categories c ON c.id=b.category_id LEFT JOIN reviews r ON r.business_id=b.id WHERE b.is_active=1 AND b.is_featured=1 GROUP BY b.id ORDER BY avg DESC,rcnt DESC LIMIT 6"); + +$alerts=qall("SELECT a.*,b.name bname,b.id bid,b.slug bslug FROM alerts a JOIN businesses b ON b.id=a.business_id WHERE a.is_active=1 ORDER BY a.created_at DESC LIMIT 4"); +$featured=qall("SELECT b.*,c.name cat,c.icon cat_icon,COALESCE(AVG(r.rating),0) avg,COUNT(r.id) rcnt FROM businesses b JOIN categories c ON c.id=b.category_id LEFT JOIN reviews r ON r.business_id=b.id AND r.is_approved=1 WHERE b.is_active=1 AND b.is_featured=1 GROUP BY b.id ORDER BY avg DESC,rcnt DESC,b.name LIMIT 6"); $totalBiz=(int)qval("SELECT COUNT(*) FROM businesses WHERE is_active=1"); -$upevts=qall("SELECT * FROM events WHERE status='approved' AND event_date>=date('now') ORDER BY is_featured DESC,event_date ASC LIMIT 3"); +$upevts=qall("SELECT * FROM events WHERE status='approved' AND event_date>=date('now') ORDER BY is_featured DESC,event_date ASC LIMIT 4"); + +$randomInterval = setting('home_random_interval','2hours'); +$slotKey = $randomInterval === 'day' ? date('Y-m-d') : (string)floor(time() / 7200); +$rotationLabel = $randomInterval === 'day' ? 'Refreshes daily' : 'Refreshes every two hours'; +$randomPool = qall(" + SELECT b.*,c.name cat,c.icon cat_icon,COALESCE(AVG(r.rating),0) avg,COUNT(r.id) rcnt + FROM businesses b + JOIN categories c ON c.id=b.category_id + LEFT JOIN reviews r ON r.business_id=b.id AND r.is_approved=1 + WHERE b.is_active=1 + GROUP BY b.id +"); +usort($randomPool, fn($a,$b) => strcmp(hash('sha256',$slotKey.'-'.$a['id']), hash('sha256',$slotKey.'-'.$b['id']))); +$randomBiz = array_slice($randomPool, 0, 8); + include __DIR__.'/includes/header.php'; ?> -
-
-
-
- - - - - - - - - - -
-
-
โ›ฐ MINERAL COUNTY ยท EASTERN PANHANDLE ยท WEST VIRGINIA
-

DISCOVERKEYSER

-
NORTH BRANCH POTOMAC RIVER ยท FOUNDED 1874
-
-

Where Appalachian heritage meets mountain beauty. County seat of Mineral County โ€” 3 hours from Washington D.C., a world away from ordinary.

-
- Explore Businesses - Attractions & Activities + +
+
+
+
+
MINERAL COUNTY ยท WEST VIRGINIA
+

KEYSERAFTER DARK

+

A neon-lit guide to local businesses, upcoming events, mountain history, and the community pages that keep Keyser moving.

+ + +
+ +
+
+
Live Directory
+
+
active Keyser business listings
+
+
+ +
+
+
+
+ +
-
-
- -
- -
-
- -
+
-
๐Ÿ“ข LOCAL BUSINESS NEWS & UPDATES
+
LOCAL BUSINESS SIGNALS
-
From ·
+
From ยท
@@ -63,26 +83,28 @@ include __DIR__.'/includes/header.php';
- -
-
-
-
OUR HISTORY
-

A City with Deep Mountain Roots

-
-

Originally called Paddy Town after Irish settler Patrick McCarty, then New Creek, Keyser took its name in 1874 โ€” honoring William Keyser, Vice President of the Baltimore & Ohio Railroad whose line transformed the region in 1842.

-

Today Keyser is home to Potomac State College of WVU (est. 1901 on historic Civil War Fort Fuller), WVU Medicine Potomac Valley Hospital, and a vibrant downtown community rich with Appalachian culture on the banks of the North Branch Potomac River.

-

Mineral County is part of the Appalachian Forest National Heritage Area โ€” over 500 square miles of forested beauty where American history lives on in every ridge and hollow.

+
+
+
+
+
UPCOMING EVENTS
+

Whatโ€™s Happening Around Keyser

- Learn More About Keyser + All Events
-
- -
-
-
-
+ +
+ +
+
+
+
+
+
+
+
FEATURED
+
+ + + +
+
160?'...':'')?>
+
-
-
- - -
-
-
๐Ÿ“… WHAT'S HAPPENING
-

Upcoming Events

-
-
- -
-
-
-
-
-
-
-
โ˜… FEATURED
-
- ๐Ÿ“ - ๐Ÿ• - ๐Ÿ’ฐ -
-
180?'โ€ฆ':'')?>
-
-
- - -
- - -
-
JOIN THE COMMUNITY
-

Own a Business in Keyser?

-

Create an account to manage your listing, post alerts, add menus, and connect with the Keyser community.

-
- - Create Account - Login - My Dashboard +
+
โ—‡
+

No upcoming approved events yet

+

Community events will appear here as they are approved.

+
+
+ +
+
+
ABOUT KEYSER
+

Railroad Roots, River Views, Mountain Pace

+
+

Keyser began as Paddy Town, then New Creek, before taking its current name in 1874 in honor of Baltimore & Ohio Railroad leader William Keyser. The rail line shaped the cityโ€™s downtown, its work ethic, and its role as Mineral Countyโ€™s county seat.

+

Today the city sits where New Creek meets the North Branch Potomac River, with Potomac State College of WVU, local restaurants, small shops, healthcare, civic life, and outdoor access all close by.

+
+ Learn More About Keyser +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
RANDOM LOCAL PICKS ยท
+

Explore Something Different

+
+ View All Businesses +
+ + +
+ +
+
+
BUSINESS OWNERS
+

Claim your Keyser business page

+

Create a business-owner account, choose the listing you manage, add a verification phone number, and submit first edits or a YouTube/Vimeo video for admin review.

+
+ + Start a Claim + + Open Dashboard + +
+ diff --git a/lone-embed.php b/lone-embed.php new file mode 100644 index 0000000..5ff6ab1 --- /dev/null +++ b/lone-embed.php @@ -0,0 +1,1322 @@ + 'video/mp4', + 'm4v' => 'video/mp4', + 'webm' => 'video/webm', + 'ogv' => 'video/ogg', + 'ogg' => 'video/ogg', + 'mov' => 'video/quicktime', + 'mkv' => 'video/x-matroska', + 'avi' => 'video/x-msvideo', + 'wmv' => 'video/x-ms-wmv', + 'flv' => 'video/x-flv', + 'ts' => 'video/mp2t', + '3gp' => 'video/3gpp', + '3g2' => 'video/3gpp2', + 'm3u8' => 'application/vnd.apple.mpegurl', + 'mpd' => 'application/dash+xml', + 'mp3' => 'audio/mpeg', + 'm4a' => 'audio/mp4', + 'aac' => 'audio/aac', + 'wav' => 'audio/wav', + 'flac' => 'audio/flac', + 'oga' => 'audio/ogg', + 'opus' => 'audio/ogg', + ]; + + $ext = extension_from_url($url); + return $map[$ext] ?? ''; +} + +function classify_kind(string $url, string $mime, string $forced_mode): string +{ + $forced = strtolower($forced_mode); + if (in_array($forced, ['video', 'audio', 'hls', 'dash', 'iframe'], true)) { + return $forced; + } + + $ext = extension_from_url($url); + $mime_lc = strtolower($mime); + + if ($ext === 'm3u8' || in_array($mime_lc, ['application/x-mpegurl', 'application/vnd.apple.mpegurl'], true)) { + return 'hls'; + } + + if ($ext === 'mpd' || $mime_lc === 'application/dash+xml') { + return 'dash'; + } + + if (starts_with($mime_lc, 'audio/')) { + return 'audio'; + } + + if (starts_with($mime_lc, 'video/')) { + return 'video'; + } + + if (in_array($ext, ['mp3', 'm4a', 'aac', 'wav', 'flac', 'oga', 'opus'], true)) { + return 'audio'; + } + + if (in_array($ext, ['mp4', 'm4v', 'webm', 'ogv', 'ogg', 'mov', 'mkv', 'avi', 'wmv', 'flv', 'ts', '3gp', '3g2'], true)) { + return 'video'; + } + + return 'unknown'; +} + +function normalize_url(string $url): string +{ + $url = trim(html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8')); + if ($url === '') { + return ''; + } + + if (starts_with($url, '//')) { + $url = 'https:' . $url; + } + + $scheme = parse_url($url, PHP_URL_SCHEME); + if (!is_string($scheme) || $scheme === '') { + return $url; + } + + $scheme = strtolower($scheme); + if (!in_array($scheme, ['http', 'https'], true)) { + return ''; + } + + return $url; +} + +function append_query(string $base, array $params): string +{ + $query = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + if ($query === '') { + return $base; + } + + return $base . (strpos($base, '?') === false ? '?' : '&') . $query; +} + +function parse_youtube_time(string $value): int +{ + $value = trim($value); + if ($value === '') { + return 0; + } + + if (ctype_digit($value)) { + return max(0, (int) $value); + } + + $seconds = 0; + if (preg_match_all('/(\d+)\s*([hms])/i', $value, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $amount = (int) $match[1]; + $unit = strtolower($match[2]); + if ($unit === 'h') { + $seconds += $amount * 3600; + } elseif ($unit === 'm') { + $seconds += $amount * 60; + } else { + $seconds += $amount; + } + } + } + + return max(0, $seconds); +} + +function clean_token(string $value, string $pattern = '/[^A-Za-z0-9_-]/'): string +{ + $clean = preg_replace($pattern, '', trim($value)); + return is_string($clean) ? $clean : ''; +} + +function twitch_parent_host(): string +{ + $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost'; + $host = strtolower(trim((string) $host)); + $host = preg_replace('/:\d+$/', '', $host); + $host = is_string($host) ? $host : 'localhost'; + return $host !== '' ? $host : 'localhost'; +} + +function provider_embed(string $url): ?array +{ + $parts = @parse_url($url); + if (!is_array($parts) || empty($parts['host'])) { + return null; + } + + $host = strtolower((string) $parts['host']); + $path = isset($parts['path']) ? (string) $parts['path'] : ''; + $segments = path_segments($url); + $query = get_query_values($url); + + if (host_matches($host, 'youtu.be') || host_matches($host, 'youtube.com') || host_matches($host, 'youtube-nocookie.com')) { + $id = ''; + if (host_matches($host, 'youtu.be') && isset($segments[0])) { + $id = $segments[0]; + } + if ($id === '' && isset($query['v'])) { + $id = scalar_string($query['v']); + } + if ($id === '' && preg_match('#/(?:embed|shorts|live|v)/([^/?#]+)#', $path, $m)) { + $id = $m[1]; + } + + $id = clean_token($id); + if ($id !== '') { + $params = ['rel' => '0', 'modestbranding' => '1', 'playsinline' => '1']; + $start = 0; + if (isset($query['start'])) { + $start = parse_youtube_time(scalar_string($query['start'])); + } elseif (isset($query['t'])) { + $start = parse_youtube_time(scalar_string($query['t'])); + } + if ($start > 0) { + $params['start'] = (string) $start; + } + + return [ + 'provider' => 'youtube', + 'embed' => append_query('https://www.youtube-nocookie.com/embed/' . rawurlencode($id), $params), + ]; + } + + if (isset($query['list'])) { + $list = clean_token(scalar_string($query['list'])); + if ($list !== '') { + return [ + 'provider' => 'youtube', + 'embed' => append_query('https://www.youtube-nocookie.com/embed/videoseries', [ + 'list' => $list, + 'rel' => '0', + 'modestbranding' => '1', + 'playsinline' => '1', + ]), + ]; + } + } + } + + if (host_matches($host, 'vimeo.com')) { + $id = ''; + if (preg_match('#/(?:video/)?(\d+)#', $path, $m)) { + $id = $m[1]; + } + + if ($id !== '') { + return [ + 'provider' => 'vimeo', + 'embed' => append_query('https://player.vimeo.com/video/' . rawurlencode($id), [ + 'title' => '0', + 'byline' => '0', + 'portrait' => '0', + ]), + ]; + } + } + + if (host_matches($host, 'dailymotion.com') || host_matches($host, 'dai.ly')) { + $id = ''; + if (host_matches($host, 'dai.ly') && isset($segments[0])) { + $id = $segments[0]; + } + if ($id === '' && preg_match('#/(?:embed/)?video/([^/?#_]+)#', $path, $m)) { + $id = $m[1]; + } + + $id = clean_token($id); + if ($id !== '') { + return [ + 'provider' => 'dailymotion', + 'embed' => 'https://www.dailymotion.com/embed/video/' . rawurlencode($id), + ]; + } + } + + if (host_matches($host, 'twitch.tv')) { + $parent = twitch_parent_host(); + + if (preg_match('#/videos/(\d+)#', $path, $m)) { + return [ + 'provider' => 'twitch', + 'embed' => append_query('https://player.twitch.tv/', [ + 'video' => $m[1], + 'parent' => $parent, + 'autoplay' => 'false', + ]), + ]; + } + + if (preg_match('#/([^/]+)/clip/([^/?#]+)#', $path, $m)) { + return [ + 'provider' => 'twitch', + 'embed' => append_query('https://clips.twitch.tv/embed', [ + 'clip' => clean_token($m[2]), + 'parent' => $parent, + 'autoplay' => 'false', + ]), + ]; + } + + if (isset($segments[0]) && $segments[0] !== '') { + return [ + 'provider' => 'twitch', + 'embed' => append_query('https://player.twitch.tv/', [ + 'channel' => clean_token($segments[0]), + 'parent' => $parent, + 'autoplay' => 'false', + ]), + ]; + } + } + + if (host_matches($host, 'clips.twitch.tv') && isset($segments[0])) { + return [ + 'provider' => 'twitch', + 'embed' => append_query('https://clips.twitch.tv/embed', [ + 'clip' => clean_token($segments[0]), + 'parent' => twitch_parent_host(), + 'autoplay' => 'false', + ]), + ]; + } + + if (host_matches($host, 'facebook.com') || host_matches($host, 'fb.watch')) { + return [ + 'provider' => 'facebook', + 'embed' => append_query('https://www.facebook.com/plugins/video.php', [ + 'href' => $url, + 'show_text' => 'false', + 'width' => '1280', + ]), + ]; + } + + if (host_matches($host, 'tiktok.com') && preg_match('#/video/(\d+)#', $path, $m)) { + return [ + 'provider' => 'tiktok', + 'embed' => 'https://www.tiktok.com/embed/v2/' . rawurlencode($m[1]), + ]; + } + + if (host_matches($host, 'instagram.com') && preg_match('#/(p|reel|tv)/([^/?#]+)#', $path, $m)) { + return [ + 'provider' => 'instagram', + 'embed' => 'https://www.instagram.com/' . rawurlencode($m[1]) . '/' . rawurlencode($m[2]) . '/embed', + ]; + } + + if ((host_matches($host, 'twitter.com') || host_matches($host, 'x.com')) && preg_match('#/status/(\d+)#', $path, $m)) { + return [ + 'provider' => 'twitter', + 'embed' => append_query('https://platform.twitter.com/embed/Tweet.html', ['id' => $m[1]]), + ]; + } + + if (host_matches($host, 'soundcloud.com')) { + return [ + 'provider' => 'soundcloud', + 'embed' => append_query('https://w.soundcloud.com/player/', [ + 'url' => $url, + 'auto_play' => 'false', + 'show_teaser' => 'true', + ]), + ]; + } + + if (host_matches($host, 'open.spotify.com') && isset($segments[0], $segments[1])) { + $type = clean_token($segments[0]); + $id = clean_token($segments[1]); + if (in_array($type, ['album', 'artist', 'episode', 'playlist', 'show', 'track'], true) && $id !== '') { + return [ + 'provider' => 'spotify', + 'embed' => 'https://open.spotify.com/embed/' . rawurlencode($type) . '/' . rawurlencode($id), + ]; + } + } + + if (host_matches($host, 'drive.google.com')) { + $id = ''; + if (preg_match('#/file/d/([^/]+)#', $path, $m)) { + $id = $m[1]; + } elseif (isset($query['id'])) { + $id = scalar_string($query['id']); + } + + $id = clean_token($id); + if ($id !== '') { + return [ + 'provider' => 'google-drive', + 'embed' => 'https://drive.google.com/file/d/' . rawurlencode($id) . '/preview', + ]; + } + } + + if ((host_matches($host, 'wistia.com') || host_matches($host, 'wi.st')) && preg_match('#/(?:medias/)?([A-Za-z0-9]+)#', $path, $m)) { + return [ + 'provider' => 'wistia', + 'embed' => 'https://fast.wistia.net/embed/iframe/' . rawurlencode($m[1]), + ]; + } + + if (host_matches($host, 'streamable.com') && isset($segments[0])) { + return [ + 'provider' => 'streamable', + 'embed' => 'https://streamable.com/e/' . rawurlencode(clean_token($segments[0])), + ]; + } + + if (host_matches($host, 'loom.com') && isset($segments[0], $segments[1]) && $segments[0] === 'share') { + return [ + 'provider' => 'loom', + 'embed' => 'https://www.loom.com/embed/' . rawurlencode(clean_token($segments[1])), + ]; + } + + return null; +} + +function rewrite_direct_url(string $url): string +{ + $host = strtolower((string) (parse_url($url, PHP_URL_HOST) ?? '')); + $path = (string) (parse_url($url, PHP_URL_PATH) ?? ''); + + if (host_matches($host, 'dropbox.com')) { + $parts = @parse_url($url); + if (is_array($parts)) { + $query = []; + if (isset($parts['query'])) { + parse_str((string) $parts['query'], $query); + } + unset($query['dl'], $query['raw']); + $query['raw'] = '1'; + + $rebuilt = ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? 'www.dropbox.com'); + $rebuilt .= $parts['path'] ?? ''; + $rebuilt = append_query($rebuilt, $query); + if (isset($parts['fragment'])) { + $rebuilt .= '#' . $parts['fragment']; + } + return $rebuilt; + } + } + + if (host_matches($host, 'github.com') && preg_match('#^/([^/]+)/([^/]+)/blob/([^/]+)/(.+)$#', $path, $m)) { + return 'https://raw.githubusercontent.com/' . rawurlencode($m[1]) . '/' . rawurlencode($m[2]) . '/' . rawurlencode($m[3]) . '/' . str_replace('%2F', '/', rawurlencode($m[4])); + } + + return $url; +} + +function source_from_raw(string $raw, string $forced_mode): ?array +{ + $raw = trim($raw); + if ($raw === '') { + return null; + } + + $mime = ''; + if (strpos($raw, '|') !== false) { + [$raw, $mime] = explode('|', $raw, 2); + $mime = trim($mime); + } + + $original_url = normalize_url($raw); + if ($original_url === '') { + return null; + } + + $provider = provider_embed($original_url); + $url = $provider ? $original_url : rewrite_direct_url($original_url); + if ($mime === '') { + $mime = guess_mime($url); + } + + $kind = classify_kind($url, $mime, $forced_mode); + if ($provider && strtolower($forced_mode) === 'auto') { + $kind = 'iframe'; + } + + if (strtolower($forced_mode) === 'iframe') { + $kind = 'iframe'; + } + + $ext = extension_from_url($url); + $label = $provider['provider'] ?? ($ext !== '' ? $ext : ($kind === 'unknown' ? 'url' : $kind)); + + return [ + 'url' => $url, + 'originalUrl' => $original_url, + 'embed' => $provider['embed'] ?? $url, + 'provider' => $provider['provider'] ?? '', + 'mime' => $mime, + 'kind' => $kind, + 'label' => $label, + ]; +} + +function query_bool(string $name, bool $default = false): bool +{ + if (!array_key_exists($name, $_GET)) { + return $default; + } + + $value = strtolower(trim((string) $_GET[$name])); + if (in_array($value, ['1', 'true', 'yes', 'on'], true)) { + return true; + } + if (in_array($value, ['0', 'false', 'no', 'off'], true)) { + return false; + } + + return $default; +} + +function media_type_attr(array $source): string +{ + $mime = isset($source['mime']) ? trim((string) $source['mime']) : ''; + return $mime !== '' ? ' type="' . h($mime) . '"' : ''; +} + +function media_common_attrs(array $config): string +{ + $attrs = []; + + if (($config['controls'] ?? true) !== false) { + $attrs[] = 'controls'; + } + if (($config['autoplay'] ?? false) === true) { + $attrs[] = 'autoplay'; + } + if (($config['muted'] ?? false) === true || ($config['autoplay'] ?? false) === true) { + $attrs[] = 'muted'; + } + if (($config['loop'] ?? false) === true) { + $attrs[] = 'loop'; + } + + $attrs[] = 'preload="metadata"'; + + $crossorigin = trim((string) ($config['crossorigin'] ?? '')); + if ($crossorigin !== '') { + $attrs[] = 'crossorigin="' . h($crossorigin) . '"'; + } + + return implode(' ', $attrs); +} + +$title = trim((string) ($_GET['title'] ?? '')); +$poster = normalize_url((string) ($_GET['poster'] ?? '')); +$mode = strtolower(trim((string) ($_GET['mode'] ?? 'auto'))); +if (!in_array($mode, ['auto', 'video', 'audio', 'hls', 'dash', 'iframe'], true)) { + $mode = 'auto'; +} + +$primary_raw = ''; +foreach (['video', 'url', 'src', 'v', 'slug'] as $key) { + if (isset($_GET[$key]) && trim((string) $_GET[$key]) !== '') { + $primary_raw = trim((string) $_GET[$key]); + break; + } +} + +$sources = []; +if ($primary_raw !== '') { + $source = source_from_raw($primary_raw, $mode); + if ($source !== null) { + $sources[] = $source; + } +} + +for ($i = 2; $i <= 10; $i++) { + foreach (["video{$i}", "url{$i}", "src{$i}"] as $key) { + if (isset($_GET[$key]) && trim((string) $_GET[$key]) !== '') { + $source = source_from_raw(trim((string) $_GET[$key]), $mode); + if ($source !== null) { + $sources[] = $source; + } + break; + } + } +} + +$crossorigin = strtolower(trim((string) ($_GET['crossorigin'] ?? ''))); +if (!in_array($crossorigin, ['anonymous', 'use-credentials'], true)) { + $crossorigin = ''; +} + +$config = [ + 'sources' => $sources, + 'title' => $title, + 'poster' => $poster, + 'autoplay' => query_bool('autoplay', false), + 'muted' => query_bool('muted', false), + 'loop' => query_bool('loop', false), + 'controls' => query_bool('controls', true), + 'playsinline' => true, + 'iframeFallback' => query_bool('iframe_fallback', true), + 'crossorigin' => $crossorigin, + 'withCredentials' => query_bool('with_credentials', false), +]; + +$primary_source = $sources[0] ?? null; + +$json_config = json_encode( + $config, + JSON_UNESCAPED_SLASHES + | JSON_UNESCAPED_UNICODE + | JSON_HEX_TAG + | JSON_HEX_AMP + | JSON_HEX_APOS + | JSON_HEX_QUOT +); +if (!is_string($json_config)) { + $json_config = '{"sources":[]}'; +} + +header_remove('X-Frame-Options'); +header_remove('Content-Security-Policy'); +header_remove('X-Content-Security-Policy'); +header('Content-Type: text/html; charset=utf-8'); +header("Content-Security-Policy: default-src * data: blob: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; frame-src *; connect-src *"); +header('Referrer-Policy: no-referrer-when-downgrade'); +header('Cross-Origin-Resource-Policy: cross-origin'); +header('X-Content-Type-Options: nosniff'); +?> + + + + + + <?= $title !== '' ? h($title) . ' - Player' : 'Player' ?> + + + + + +
+
+ + 0): ?> +
+
+ +
+ +
+ +
+ + +
+
+
style="background-image:url('')">
+ +
+
+ + + + +
+
+ + + +
+
+ + + + + + + +
+
+

No media URL supplied

+

Use ?video=https%3A%2F%2Fexample.com%2Fclip.mp4 or ?url=....

+
+
+ +
+ + 0): ?> + + + + diff --git a/register.php b/register.php index c677d49..6890d42 100644 --- a/register.php +++ b/register.php @@ -8,286 +8,262 @@ if (setting('registration_enabled','1') !== '1') { $pageTitle = 'Create Account โ€” Discover Keyser WV'; $errors = []; +$businessOptions = qall("SELECT id,name,address FROM businesses WHERE is_active=1 ORDER BY name"); +$tier = p('member_type', 'member') === 'business_owner' ? 'business_owner' : 'member'; if (isPost()) { csrfCheck(); - // โ”€โ”€ Honeypot check โ€” bots fill hidden fields, humans don't โ”€โ”€ - // Field named "website_url" is visually hidden; any value = bot - if (ps('website_url') !== '') { - // Silently succeed (don't tell the bot it failed) + if (ps('website_url') !== '' || ps('confirm_email') !== '') { flash('Account created! Welcome to Discover Keyser WV.','success'); go('/index.php'); } - // โ”€โ”€ Timing check โ€” real humans take at least 3 seconds โ”€โ”€ $formTime = (int)p('_form_time', 0); if ($formTime && (time() - $formTime) < 3) { - // Also silent โ€” pretend success flash('Account created! Welcome to Discover Keyser WV.','success'); go('/index.php'); } + $tier = p('member_type', 'member') === 'business_owner' ? 'business_owner' : 'member'; $un = ps('username'); $em = ps('email'); $pw = ps('password'); $pw2 = ps('password2'); $tos = p('lawful_use', ''); - if (strlen($un) < 3) - $errors[] = 'Username must be at least 3 characters.'; - if (!preg_match('/^[a-zA-Z0-9_]+$/', $un)) - $errors[] = 'Username may only contain letters, numbers, and underscores.'; - if ($em && !filter_var($em, FILTER_VALIDATE_EMAIL)) - $errors[] = 'Please enter a valid email address.'; - if (strlen($pw) < 6) - $errors[] = 'Password must be at least 6 characters.'; - if ($pw !== $pw2) - $errors[] = 'Passwords do not match.'; - if ($tos !== '1') - $errors[] = 'You must agree to use this service lawfully to create an account.'; - if (!$errors && qval("SELECT id FROM users WHERE username=?", [$un])) - $errors[] = 'That username is already taken. Please choose another.'; - if (!$errors && $em && qval("SELECT id FROM users WHERE email=?", [$em])) - $errors[] = 'That email address is already registered. Try signing in instead.'; + $claimBizId = (int)p('claim_business_id'); + $contactPhone = ps('contact_phone'); + $claimBiz = null; + + if (strlen($un) < 3) $errors[] = 'Username must be at least 3 characters.'; + if (!preg_match('/^[a-zA-Z0-9_]+$/', $un)) $errors[] = 'Username may only contain letters, numbers, and underscores.'; + if ($em && !filter_var($em, FILTER_VALIDATE_EMAIL)) $errors[] = 'Please enter a valid email address.'; + if (strlen($pw) < 6) $errors[] = 'Password must be at least 6 characters.'; + if ($pw !== $pw2) $errors[] = 'Passwords do not match.'; + if ($tos !== '1') $errors[] = 'You must agree to use this service lawfully to create an account.'; + if (!$errors && qval("SELECT id FROM users WHERE username=?", [$un])) $errors[] = 'That username is already taken. Please choose another.'; + if (!$errors && $em && qval("SELECT id FROM users WHERE email=?", [$em])) $errors[] = 'That email address is already registered. Try signing in instead.'; + + $claimEdits = [ + 'description' => ps('claim_description'), + 'address' => ps('claim_address'), + 'phone' => ps('claim_public_phone'), + 'email' => ps('claim_public_email'), + 'website' => ps('claim_website'), + 'video_url' => ps('claim_video_url'), + ]; + + if ($tier === 'business_owner') { + if (!$claimBizId) { + $errors[] = 'Please select the business page you want to claim.'; + } else { + $claimBiz = qone("SELECT * FROM businesses WHERE id=? AND is_active=1", [$claimBizId]); + if (!$claimBiz) $errors[] = 'Please select a valid listed business.'; + } + if ($contactPhone === '' || strlen(preg_replace('/\D+/', '', $contactPhone)) < 7) { + $errors[] = 'Please enter a contact phone number for verification.'; + } + if ($claimEdits['email'] !== '' && !filter_var($claimEdits['email'], FILTER_VALIDATE_EMAIL)) { + $errors[] = 'Please enter a valid public email edit.'; + } + if (!validBusinessVideoUrl($claimEdits['video_url'])) { + $errors[] = 'Business videos must be a YouTube or Vimeo URL.'; + } + } if (!$errors) { $id = qrun( "INSERT INTO users(username,email,password)VALUES(?,?,?)", [$un, $em ?: null, password_hash($pw, PASSWORD_BCRYPT)] ); + + $submittedEdits = 0; + if ($tier === 'business_owner' && $claimBiz) { + qrun( + "INSERT INTO business_claim_requests(business_id,user_id,contact_phone)VALUES(?,?,?)", + [$claimBizId, $id, $contactPhone] + ); + + foreach ($claimEdits as $field => $newValue) { + if ($newValue === '') continue; + $oldValue = (string)($claimBiz[$field] ?? ''); + if ($newValue === $oldValue) continue; + qrun( + "INSERT INTO business_edit_requests(business_id,user_id,field,old_value,new_value)VALUES(?,?,?,?,?)", + [$claimBizId, $id, $field, $oldValue, $newValue] + ); + $submittedEdits++; + } + } + loginUser(qone("SELECT * FROM users WHERE id=?", [$id])); - flash('Welcome to Discover Keyser WV, '.e($un).'!', 'success'); + + if ($tier === 'business_owner' && $claimBiz) { + $msg = 'Your account is ready. Your claim for '.$claimBiz['name'].' is queued, and we will contact you at '.$contactPhone.' within a few business days for verification.'; + if ($submittedEdits) $msg .= ' '.$submittedEdits.' proposed edit'.($submittedEdits !== 1 ? 's were' : ' was').' sent to admin review.'; + flash($msg, 'success'); + go('/dashboard.php'); + } + + flash('Welcome to Discover Keyser WV, '.$un.'!', 'success'); go('/index.php'); } } +$ownerSelected = $tier === 'business_owner'; + +$footExtra = << +(() => { + const radios = document.querySelectorAll('input[name="member_type"]'); + const ownerFields = document.getElementById('ownerClaimFields'); + const sync = () => { + const owner = document.querySelector('input[name="member_type"]:checked')?.value === 'business_owner'; + ownerFields?.classList.toggle('is-open', owner); + ownerFields?.querySelectorAll('[data-owner-required]').forEach(el => { + if (owner) el.setAttribute('required', 'required'); + else el.removeAttribute('required'); + }); + }; + radios.forEach(r => r.addEventListener('change', sync)); + sync(); +})(); + +HTML; + include __DIR__.'/includes/header.php'; ?> - - -
- - -
-
JOIN THE COMMUNITY
-

Create Your Account

-

- Discover Keyser WV is your community hub for local businesses, events, and information. - Takes less than a minute. -

+