- initial
This commit is contained in:
+204
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$slug=gs('slug');
|
||||
$b=qone("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.slug=? AND b.is_active=1 GROUP BY b.id",[$slug]);
|
||||
if(!$b){flash('Business not found.','error');go('/directory.php');}
|
||||
|
||||
if(isPost()){
|
||||
csrfCheck();
|
||||
$act=p('_act');
|
||||
if($act==='review'&&authed()){
|
||||
$rating=max(1,min(5,(int)p('rating',5)));
|
||||
$title=ps('rtitle'); $body=ps('rbody');
|
||||
$ex=qval("SELECT id FROM reviews WHERE business_id=? AND user_id=?",[$b['id'],uid()]);
|
||||
if($ex) qrun("UPDATE reviews SET rating=?,title=?,body=?,created_at=datetime('now') WHERE id=?",[$rating,$title,$body,$ex]);
|
||||
else qrun("INSERT INTO reviews(business_id,user_id,rating,title,body)VALUES(?,?,?,?,?)",[$b['id'],uid(),$rating,$title,$body]);
|
||||
flash('Review saved!','success'); go("/business.php?slug=$slug");
|
||||
}
|
||||
if($act==='post_alert'&&owns($b['id'])){
|
||||
$t=ps('atitle');$body=ps('abody');$type=p('atype','info');
|
||||
if($t&&$body){qrun("INSERT INTO alerts(business_id,type,title,body)VALUES(?,?,?,?)",[$b['id'],$type,$t,$body]);flash('Alert posted!','success');}
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
if($act==='del_alert'&&owns($b['id'])){
|
||||
qrun("DELETE FROM alerts WHERE id=? AND business_id=?",[(int)p('aid'),$b['id']]);
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
if($act==='del_review'&&isAdmin()){
|
||||
qrun("DELETE FROM reviews WHERE id=?",[(int)p('rid')]);
|
||||
flash('Review deleted.','success'); go("/business.php?slug=$slug");
|
||||
}
|
||||
if($act==='report'&&authed()){
|
||||
$rtype=p('rtype','correction');$rmsg=ps('rmsg');
|
||||
if($rmsg){qrun("INSERT INTO reports(business_id,user_id,type,message)VALUES(?,?,?,?)",[$b['id'],uid(),$rtype,$rmsg]);flash('Report submitted. Thank you!','success');}
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
}
|
||||
|
||||
$pageTitle=e($b['name']).' — Keyser, WV'; $activeNav='dir';
|
||||
$reviews=qall("SELECT r.*,u.username FROM reviews r JOIN users u ON u.id=r.user_id WHERE r.business_id=? AND r.is_approved=1 ORDER BY r.created_at DESC",[$b['id']]);
|
||||
$sects=qall("SELECT * FROM menu_sections WHERE business_id=? ORDER BY sort_order",[$b['id']]);
|
||||
$mitems=[];
|
||||
foreach($sects as $s) $mitems[$s['id']]=qall("SELECT * FROM menu_items WHERE section_id=? AND is_available=1 ORDER BY sort_order",[$s['id']]);
|
||||
$alerts=qall("SELECT * FROM alerts WHERE business_id=? AND is_active=1 ORDER BY created_at DESC",[$b['id']]);
|
||||
$myrev=authed()?qone("SELECT * FROM reviews WHERE business_id=? AND user_id=?",[$b['id'],uid()]):null;
|
||||
$hours=parseHours($b['hours']); $isOwner=owns($b['id']);
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="detail-hdr">
|
||||
<div class="detail-hdr-inner">
|
||||
<div class="breadcrumb"><a href="/index.php">Home</a><span>›</span><a href="/directory.php">Directory</a><span>›</span><a href="/directory.php?cat=<?=urlencode($b['cat'])?>"><?=e($b['cat_icon'].' '.$b['cat'])?></a><span>›</span><?=e($b['name'])?></div>
|
||||
<div class="cat-badge"><?=e($b['cat_icon'].' '.$b['cat'])?><?=$b['subcategory']?' · '.e($b['subcategory']):''?></div>
|
||||
<h1 style="font-size:clamp(1.9rem,4vw,2.9rem);margin:.4rem 0"><?=e($b['name'])?></h1>
|
||||
<div class="flex-row" style="margin-top:.6rem;gap:.9rem">
|
||||
<?=starDisplay((float)$b['avg'])?><span class="rat-score"><?=number_format((float)$b['avg'],1)?></span><span class="rat-cnt">(<?=$b['rcnt']?> review<?=$b['rcnt']!=1?'s':''?>)</span>
|
||||
<?php if($b['is_featured']): ?><span class="featured-badge">★ FEATURED</span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<div>
|
||||
<?php if($alerts): ?>
|
||||
<div style="margin-bottom:1.75rem">
|
||||
<div class="eyebrow" style="margin-bottom:.6rem">📢 BUSINESS UPDATES</div>
|
||||
<?php foreach($alerts as $a): ?>
|
||||
<div class="alert-item a-<?=e($a['type'])?>">
|
||||
<div class="alert-ico"><?=alertIcon($a['type'])?></div>
|
||||
<div style="flex:1"><div class="alert-title"><?=e($a['title'])?></div><div class="alert-body"><?=e($a['body'])?></div><div class="alert-meta"><?=ago($a['created_at'])?></div></div>
|
||||
<?php if($isOwner): ?>
|
||||
<form method="POST" style="margin-left:.5rem"><?=csrfField()?><input type="hidden" name="_act" value="del_alert"><input type="hidden" name="aid" value="<?=$a['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete this alert?">✕</button></form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($isOwner): ?>
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:1.75rem">
|
||||
<div class="ibox-title">📢 POST BUSINESS ALERT</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="post_alert">
|
||||
<div class="form-row" style="margin-bottom:.7rem">
|
||||
<input type="text" name="atitle" class="finput" placeholder="Alert title" required>
|
||||
<select name="atype" class="fselect"><option value="info">ℹ️ Info</option><option value="news">📰 News</option><option value="event">🎉 Event</option><option value="warning">⚠️ Warning</option><option value="sale">🏷️ Sale/Promo</option></select>
|
||||
</div>
|
||||
<textarea name="abody" class="ftextarea" style="min-height:65px" placeholder="Alert details…" required></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top:.65rem">Post Alert</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ibox" style="margin-bottom:1.75rem">
|
||||
<div class="ibox-title">ABOUT <?=strtoupper(e($b['name']))?></div>
|
||||
<p style="color:var(--text-m);line-height:1.85"><?=e($b['description'])?></p>
|
||||
</div>
|
||||
|
||||
<?php if($sects): ?>
|
||||
<div class="ibox" style="margin-bottom:1.75rem">
|
||||
<div class="ibox-title">🍽️ MENU</div>
|
||||
<?php foreach($sects as $s): ?>
|
||||
<div class="menu-sec">
|
||||
<div class="menu-sec-name"><?=e($s['name'])?></div>
|
||||
<?php if($s['description']): ?><div class="menu-sec-desc"><?=e($s['description'])?></div><?php endif; ?>
|
||||
<?php foreach($mitems[$s['id']] as $mi): ?>
|
||||
<div class="menu-item">
|
||||
<div class="mi-info"><div class="mi-name"><?=e($mi['name'])?></div><?php if($mi['description']): ?><div class="mi-desc"><?=e($mi['description'])?></div><?php endif; ?></div>
|
||||
<?php if($mi['price']>0): ?><div class="mi-price"><?=money((float)$mi['price'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if($isOwner): ?><a href="/menu.php?id=<?=$b['id']?>" class="btn btn-outline btn-sm">✏️ Manage Menu</a><?php endif; ?>
|
||||
</div>
|
||||
<?php elseif($isOwner): ?>
|
||||
<div style="margin-bottom:1.75rem"><a href="/menu.php?id=<?=$b['id']?>" class="btn btn-outline">+ Add Menu</a></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3 style="font-family:'Oswald',sans-serif;letter-spacing:.1em;font-size:1.05rem;color:var(--text);margin-bottom:1.1rem;padding-bottom:.6rem;border-bottom:1px solid var(--bdr)">⭐ REVIEWS (<?=$b['rcnt']?>)</h3>
|
||||
|
||||
<?php if(authed()): ?>
|
||||
<div class="ibox" style="margin-bottom:1.5rem">
|
||||
<div class="ibox-title"><?=$myrev?'UPDATE YOUR REVIEW':'WRITE A REVIEW'?></div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="review">
|
||||
<div class="fg"><label class="flabel">YOUR RATING</label><?=starPicker('rating',(int)($myrev['rating']??0))?></div>
|
||||
<div class="fg"><label class="flabel">TITLE (optional)</label><input type="text" name="rtitle" class="finput" value="<?=e($myrev['title']??'')?>" placeholder="One-line summary…"></div>
|
||||
<div class="fg"><label class="flabel">YOUR REVIEW</label><textarea name="rbody" class="ftextarea" placeholder="Share your experience…"><?=e($myrev['body']??'')?></textarea></div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Submit Review</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="background:var(--bg4);border:1px solid var(--bdr);border-radius:6px;padding:1.1rem;text-align:center;margin-bottom:1.4rem">
|
||||
<a href="/login.php" class="btn btn-outline btn-sm">Login to Write a Review</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach($reviews as $r): ?>
|
||||
<div class="rev-card">
|
||||
<div class="rev-hdr">
|
||||
<div><div class="rev-author"><?=e($r['username'])?></div><?=starDisplay((float)$r['rating'])?></div>
|
||||
<div class="flex-row" style="gap:.5rem">
|
||||
<span class="rev-date"><?=ago($r['created_at'])?></span>
|
||||
<?php if(isAdmin()): ?><form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="del_review"><input type="hidden" name="rid" value="<?=$r['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete review?">Delete</button></form><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if($r['title']): ?><div class="rev-title"><?=e($r['title'])?></div><?php endif; ?>
|
||||
<?php if($r['body']): ?><div class="rev-body"><?=e($r['body'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if(!$reviews): ?><p style="color:var(--text-d);font-size:.88rem">No reviews yet — be the first!</p><?php endif; ?>
|
||||
|
||||
<hr class="divider">
|
||||
<?php if(authed()): ?>
|
||||
<details>
|
||||
<summary style="cursor:pointer;font-size:.82rem;color:var(--text-d);font-family:'Oswald',sans-serif;letter-spacing:.06em">🚩 REPORT A LISTING ISSUE</summary>
|
||||
<div class="ibox" style="margin-top:.75rem">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="report">
|
||||
<div class="fg"><label class="flabel">ISSUE TYPE</label><select name="rtype" class="fselect"><option value="correction">Incorrect Information</option><option value="closed">Business Closed/Moved</option><option value="hours">Wrong Hours</option><option value="phone">Wrong Phone</option><option value="other">Other</option></select></div>
|
||||
<div class="fg"><label class="flabel">DESCRIBE THE ISSUE</label><textarea name="rmsg" class="ftextarea" style="min-height:75px" required placeholder="Please describe what needs correction…"></textarea></div>
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Submit Report</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<?php else: ?><p style="font-size:.82rem;color:var(--text-d)">🚩 <a href="/login.php">Log in</a> to report a listing issue.</p><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">BUSINESS INFO</div>
|
||||
<?php if($b['address']): ?><div class="irow"><span class="irow-i">📍</span><span class="irow-v"><?=e($b['address'])?></span></div><?php endif; ?>
|
||||
<?php if($b['phone']): ?><div class="irow"><span class="irow-i">📞</span><a href="tel:<?=e($b['phone'])?>" class="irow-v" style="color:var(--gold)"><?=e($b['phone'])?></a></div><?php endif; ?>
|
||||
<?php if($b['email']): ?><div class="irow"><span class="irow-i">✉️</span><a href="mailto:<?=e($b['email'])?>" class="irow-v" style="color:var(--gold)"><?=e($b['email'])?></a></div><?php endif; ?>
|
||||
<?php if($b['website']): ?><div class="irow"><span class="irow-i">🌐</span><a href="https://<?=e($b['website'])?>" target="_blank" class="irow-v" style="color:var(--gold);word-break:break-all"><?=e($b['website'])?> ↗</a></div><?php endif; ?>
|
||||
<div class="irow"><span class="irow-i">🏷️</span><span class="irow-v"><?=e($b['cat'])?><?=$b['subcategory']?' · '.e($b['subcategory']):''?></span></div>
|
||||
</div>
|
||||
|
||||
<?php if($hours): ?>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">🕐 HOURS OF OPERATION</div>
|
||||
<div class="hrs-grid">
|
||||
<?php foreach($hours as $day=>$time): ?>
|
||||
<div class="hrs-row"><span class="hrs-day"><?=strtoupper(substr(e($day),0,3))?></span><span class="hrs-time"><?=e($time)?></span></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($b['address']): ?>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">📍 LOCATION</div>
|
||||
<div style="background:var(--bg4);border:1px dashed var(--bdr);border-radius:4px;padding:1.75rem;text-align:center">
|
||||
<p style="font-size:.84rem;color:var(--text-m);margin-bottom:.75rem"><?=e($b['address'])?></p>
|
||||
<a href="https://maps.google.com/?q=<?=urlencode($b['name'].' '.$b['address'])?>" target="_blank" class="btn btn-outline btn-sm">Open in Google Maps ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($isOwner): ?>
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">⚙️ OWNER CONTROLS</div>
|
||||
<a href="/menu.php?id=<?=$b['id']?>" class="btn btn-outline btn-sm btn-block" style="margin-bottom:.5rem">🍽️ Manage Menu</a>
|
||||
<?php if(isAdmin()): ?><a href="/admin/businesses.php?edit=<?=$b['id']?>" class="btn btn-secondary btn-sm btn-block">✏️ Edit Listing (Admin)</a><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
// directory.php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$activeNav='dir'; $q=gs('q'); $cat=gs('cat'); $page=max(1,iget('page')); $perPage=18;
|
||||
$where="WHERE b.is_active=1"; $params=[];
|
||||
if($q){$where.=" AND(b.name LIKE ? OR b.description LIKE ? OR b.subcategory LIKE ? OR b.address LIKE ?)";$lk="%$q%";$params=[$lk,$lk,$lk,$lk];}
|
||||
if($cat){$where.=" AND c.name=?";$params[]=$cat;}
|
||||
$total=(int)qval("SELECT COUNT(*) FROM businesses b JOIN categories c ON c.id=b.category_id $where",$params);
|
||||
$pages=max(1,(int)ceil($total/$perPage)); $page=min($page,$pages); $offset=($page-1)*$perPage;
|
||||
$bizs=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 GROUP BY b.id ORDER BY b.is_featured DESC,b.name ASC LIMIT $perPage OFFSET $offset",$params);
|
||||
$cats=qall("SELECT DISTINCT c.name,c.icon FROM categories c JOIN businesses b ON b.category_id=c.id WHERE b.is_active=1 ORDER BY c.sort_order");
|
||||
$pageTitle='Business Directory — Keyser, WV';
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner">
|
||||
<div class="eyebrow">EXPLORE KEYSER</div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin-bottom:.4rem">Business Directory</h1>
|
||||
<p style="color:var(--text-m)">Discover <?=$total?> local businesses, restaurants, services, and more in Keyser, West Virginia.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-bar">
|
||||
<div class="search-bar-inner">
|
||||
<form method="GET" class="search-form">
|
||||
<div class="search-wrap"><span class="s-icon">🔍</span><input class="search-input" type="text" name="q" value="<?=e($q)?>" placeholder="Search businesses…"></div>
|
||||
<select name="cat" class="finput" style="flex:0 0 auto;width:auto;padding:.72rem 1rem">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach($cats as $c): ?><option value="<?=e($c['name'])?>"<?=$c['name']===$cat?' selected':''?>><?=e($c['icon'].' '.$c['name'])?></option><?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
<?php if($q||$cat): ?><a href="/directory.php" class="btn btn-secondary">Clear</a><?php endif; ?>
|
||||
</form>
|
||||
<?php if($q||$cat): ?><p style="margin-top:.65rem;font-size:.84rem;color:var(--text-m)">Found <strong style="color:var(--gold)"><?=$total?></strong> result<?=$total!==1?'s':''?><?php if($q): ?> for "<em><?=e($q)?></em>"<?php endif; ?><?php if($cat): ?> in <em><?=e($cat)?></em><?php endif; ?></p><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bar"><div class="filter-bar-inner">
|
||||
<a href="/directory.php<?=$q?'?q='.urlencode($q):''?>" class="btn btn-xs<?=!$cat?' btn-primary':' btn-outline'?>">All</a>
|
||||
<?php foreach($cats as $c): ?><a href="/directory.php?cat=<?=urlencode($c['name'])?><?=$q?'&q='.urlencode($q):''?>" class="btn btn-xs<?=$c['name']===$cat?' btn-primary':' btn-outline'?>"><?=e($c['icon'].' '.$c['name'])?></a><?php endforeach; ?>
|
||||
</div></div>
|
||||
<div class="section">
|
||||
<?php if($bizs): ?>
|
||||
<div class="g3">
|
||||
<?php foreach($bizs as $b): ?>
|
||||
<a href="/business.php?slug=<?=e($b['slug'])?>" style="text-decoration:none">
|
||||
<div class="biz-card">
|
||||
<div class="biz-card-top">
|
||||
<div class="cat-badge"><?=e($b['cat_icon'].' '.$b['cat'])?></div>
|
||||
<?php if($b['is_featured']): ?><span class="featured-badge" style="float:right">★</span><?php endif; ?>
|
||||
<div class="biz-name"><?=e($b['name'])?></div>
|
||||
<?php if($b['subcategory']): ?><div class="biz-sub"><?=e($b['subcategory'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="biz-card-body">
|
||||
<div class="biz-desc"><?=e($b['description'])?></div>
|
||||
<div class="biz-meta">
|
||||
<?php if($b['address']): ?><div class="biz-meta-row"><span class="biz-meta-icon">📍</span><?=e($b['address'])?></div><?php endif; ?>
|
||||
<?php if($b['phone']): ?><div class="biz-meta-row"><span class="biz-meta-icon">📞</span><?=e($b['phone'])?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="biz-card-foot">
|
||||
<div class="flex-row" style="gap:.4rem"><?=starDisplay((float)$b['avg'])?><span class="rat-score"><?=number_format((float)$b['avg'],1)?></span><span class="rat-cnt">(<?=$b['rcnt']?> review<?=$b['rcnt']!=1?'s':''?>)</span></div>
|
||||
<span style="color:var(--gold);font-size:.76rem;font-family:'Oswald',sans-serif">VIEW →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if($pages>1): ?><div style="display:flex;justify-content:center;gap:.5rem;margin-top:2.5rem;flex-wrap:wrap"><?php for($i=1;$i<=$pages;$i++): ?><a href="?<?=http_build_query(array_filter(['q'=>$q,'cat'=>$cat,'page'=>$i]))?>" class="btn btn-sm<?=$i===$page?' btn-primary':' btn-outline'?>"><?=$i?></a><?php endfor; ?></div><?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="empty"><div class="empty-ico">🔍</div><h3>No results found</h3><p style="margin-top:.5rem">Try different search terms or browse all categories.</p><a href="/directory.php" class="btn btn-outline" style="margin-top:1.5rem">Browse All</a></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
//print "Under Maintenance.";
|
||||
//die();
|
||||
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");
|
||||
$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");
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<section class="hero">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-stars"></div>
|
||||
<div class="hero-mtns">
|
||||
<svg viewBox="0 0 1440 300" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="0,300 0,180 80,120 160,160 240,80 330,138 420,58 520,118 630,38 740,98 850,18 960,88 1060,28 1160,108 1260,48 1380,128 1440,78 1440,300" fill="#0a0f18" opacity=".9"/>
|
||||
<polygon points="0,300 0,222 100,170 200,200 310,140 430,188 560,128 680,168 800,100 920,158 1030,108 1140,158 1250,118 1380,175 1440,145 1440,300" fill="#0d1520" opacity=".96"/>
|
||||
<polygon points="0,300 0,260 140,232 280,252 420,212 560,242 700,202 840,235 980,197 1120,228 1260,205 1440,225 1440,300" fill="#111c2c"/>
|
||||
<g opacity=".28" stroke="#c9a84c" stroke-width="1.5" fill="none">
|
||||
<line x1="850" y1="20" x2="850" y2="72"/><line x1="850" y1="20" x2="833" y2="8"/><line x1="850" y1="20" x2="867" y2="8"/><line x1="850" y1="20" x2="850" y2="5"/>
|
||||
<line x1="903" y1="30" x2="903" y2="82"/><line x1="903" y1="30" x2="886" y2="18"/><line x1="903" y1="30" x2="920" y2="18"/><line x1="903" y1="30" x2="903" y2="15"/>
|
||||
<line x1="958" y1="42" x2="958" y2="94"/><line x1="958" y1="42" x2="941" y2="30"/><line x1="958" y1="42" x2="975" y2="30"/><line x1="958" y1="42" x2="958" y2="27"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-eyebrow">⛰ MINERAL COUNTY · EASTERN PANHANDLE · WEST VIRGINIA</div>
|
||||
<h1 class="hero-h1">DISCOVER<span class="gld">KEYSER</span></h1>
|
||||
<div class="hero-sub">NORTH BRANCH POTOMAC RIVER · FOUNDED 1874</div>
|
||||
<div class="hero-rule"></div>
|
||||
<p class="hero-desc">Where Appalachian heritage meets mountain beauty. County seat of Mineral County — 3 hours from Washington D.C., a world away from ordinary.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/directory.php" class="btn btn-primary">Explore Businesses</a>
|
||||
<a href="/attractions.php" class="btn btn-outline">Attractions & Activities</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="stats-strip">
|
||||
<div class="stats-inner">
|
||||
<?php foreach([['1874','Founded'],['809 ft','Elevation'],['~4,800','Population'],['132','Wind Turbines'],['1901','PSC Founded'],[$totalBiz,'Businesses Listed']] as [$n,$l]): ?>
|
||||
<div class="stat"><div class="stat-n"><?=e($n)?></div><div class="stat-l"><?=e($l)?></div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($alerts): ?>
|
||||
<div class="alert-strip">
|
||||
<div class="alert-strip-inner">
|
||||
<div class="eyebrow" style="margin-bottom:.65rem">📢 LOCAL BUSINESS NEWS & UPDATES</div>
|
||||
<?php foreach($alerts as $a): ?>
|
||||
<div class="alert-item a-<?=e($a['type'])?>">
|
||||
<div class="alert-ico"><?=alertIcon($a['type'])?></div>
|
||||
<div>
|
||||
<div class="alert-title"><?=e($a['title'])?></div>
|
||||
<div class="alert-body"><?=e($a['body'])?></div>
|
||||
<div class="alert-meta">From <a href="/business.php?slug=<?=e($a['bslug'])?>"><?=e($a['bname'])?></a> · <?=ago($a['created_at'])?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="section">
|
||||
<div style="margin-bottom:2rem">
|
||||
<div class="eyebrow">⭐ COMMUNITY FAVORITES</div>
|
||||
<h2 class="sec-title">Featured Businesses</h2>
|
||||
<div class="sec-rule"></div>
|
||||
</div>
|
||||
<div class="g3">
|
||||
<?php foreach($featured as $b): ?>
|
||||
<a href="/business.php?slug=<?=e($b['slug'])?>" style="text-decoration:none">
|
||||
<div class="biz-card">
|
||||
<div class="biz-card-top">
|
||||
<div class="cat-badge"><?=e($b['cat_icon'].' '.$b['cat'])?></div>
|
||||
<?php if($b['is_featured']): ?><span class="featured-badge" style="float:right">★ FEATURED</span><?php endif; ?>
|
||||
<div class="biz-name"><?=e($b['name'])?></div>
|
||||
<?php if($b['subcategory']): ?><div class="biz-sub"><?=e($b['subcategory'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="biz-card-body">
|
||||
<div class="biz-desc"><?=e($b['description'])?></div>
|
||||
<div class="biz-meta">
|
||||
<?php if($b['address']): ?><div class="biz-meta-row"><span class="biz-meta-icon">📍</span><?=e($b['address'])?></div><?php endif; ?>
|
||||
<?php if($b['phone']): ?><div class="biz-meta-row"><span class="biz-meta-icon">📞</span><?=e($b['phone'])?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="biz-card-foot">
|
||||
<div class="flex-row" style="gap:.4rem">
|
||||
<?=starDisplay((float)$b['avg'])?>
|
||||
<span class="rat-score"><?=number_format((float)$b['avg'],1)?></span>
|
||||
<span class="rat-cnt">(<?=$b['rcnt']?>)</span>
|
||||
</div>
|
||||
<span style="color:var(--gold);font-size:.76rem;font-family:'Oswald',sans-serif">VIEW →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:2rem">
|
||||
<a href="/directory.php" class="btn btn-outline">Browse Full Directory (<?=$totalBiz?> Businesses)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background:var(--bg2);border-top:1px solid var(--bdr);border-bottom:1px solid var(--bdr);padding:4rem 1.5rem">
|
||||
<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr 1fr;gap:3rem;align-items:center">
|
||||
<div>
|
||||
<div class="eyebrow">OUR HISTORY</div>
|
||||
<h2 class="sec-title" style="margin-bottom:1rem">A City with Deep Mountain Roots</h2>
|
||||
<div class="prose">
|
||||
<p>Originally called <strong>Paddy Town</strong> 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.</p>
|
||||
<p>Today Keyser is home to <strong>Potomac State College of WVU</strong> (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.</p>
|
||||
<p>Mineral County is part of the <strong>Appalachian Forest National Heritage Area</strong> — over 500 square miles of forested beauty where American history lives on in every ridge and hollow.</p>
|
||||
</div>
|
||||
<a href="/about.php" class="btn btn-outline" style="margin-top:1rem">Learn More About Keyser</a>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<?php foreach([['🏛️','HISTORIC','National Register of Historic Places sites'],['🎓','EDUCATION','Potomac State College of WVU since 1901'],['🌊','OUTDOORS','Kayaking & fishing the North Branch'],['💨','WIND ENERGY','132 turbines on the Allegheny Front']] as [$ic,$lbl,$val]): ?>
|
||||
<div style="background:var(--bg3);border:1px solid var(--bdr);border-radius:6px;padding:1.2rem;text-align:center">
|
||||
<div style="font-size:2rem;margin-bottom:.45rem"><?=$ic?></div>
|
||||
<div style="font-family:'Oswald',sans-serif;font-size:.66rem;letter-spacing:.2em;color:var(--gold)"><?=$lbl?></div>
|
||||
<div style="color:var(--text);margin-top:.25rem;font-size:.88rem"><?=$val?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($upevts): ?>
|
||||
<div class="section">
|
||||
<div style="margin-bottom:2rem">
|
||||
<div class="eyebrow">📅 WHAT'S HAPPENING</div>
|
||||
<h2 class="sec-title">Upcoming Events</h2>
|
||||
<div class="sec-rule"></div>
|
||||
</div>
|
||||
<?php foreach($upevts as $ev): ?>
|
||||
<div class="evt-card">
|
||||
<div class="evt-date-box">
|
||||
<div class="evt-mon"><?=strtoupper(date('M',strtotime($ev['event_date'])))?></div>
|
||||
<div class="evt-day"><?=date('j',strtotime($ev['event_date']))?></div>
|
||||
<div class="evt-yr"><?=date('Y',strtotime($ev['event_date']))?></div>
|
||||
</div>
|
||||
<div class="evt-body">
|
||||
<div class="evt-title"><?=e($ev['title'])?><?php if($ev['is_featured']): ?> <span class="featured-badge" style="margin-left:.5rem">★ FEATURED</span><?php endif; ?></div>
|
||||
<div class="evt-meta">
|
||||
<?php if($ev['venue']): ?><span>📍 <?=e($ev['venue'])?></span><?php endif; ?>
|
||||
<?php if($ev['event_time']): ?><span>🕐 <?=e($ev['event_time'])?></span><?php endif; ?>
|
||||
<span>💰 <?=e($ev['cost'])?></span>
|
||||
</div>
|
||||
<div class="evt-desc"><?=e(substr($ev['description'],0,180)).(strlen($ev['description'])>180?'…':'')?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div style="text-align:center;margin-top:1.5rem">
|
||||
<a href="/events.php" class="btn btn-outline">All Events</a>
|
||||
<a href="/events.php?submit=1" class="btn btn-secondary" style="margin-left:.75rem">Submit an Event</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="padding:4rem 1.5rem;text-align:center;max-width:560px;margin:0 auto">
|
||||
<div class="eyebrow">JOIN THE COMMUNITY</div>
|
||||
<h2 style="font-size:2rem;margin:.5rem 0 1rem">Own a Business in Keyser?</h2>
|
||||
<p style="color:var(--text-m);margin-bottom:2rem;line-height:1.85">Create an account to manage your listing, post alerts, add menus, and connect with the Keyser community.</p>
|
||||
<div class="flex-row" style="justify-content:center">
|
||||
<?php if(!authed()): ?>
|
||||
<a href="/register.php" class="btn btn-primary">Create Account</a>
|
||||
<a href="/login.php" class="btn btn-outline">Login</a>
|
||||
<?php else: ?>
|
||||
<a href="/dashboard.php" class="btn btn-primary">My Dashboard</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
if (authed()) go('/index.php');
|
||||
if (setting('registration_enabled','1') !== '1') {
|
||||
flash('Registration is currently disabled.','warning'); go('/login.php');
|
||||
}
|
||||
$pageTitle = 'Create Account — Keyser WV';
|
||||
$errors = [];
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$un = ps('username'); $em = ps('email'); $pw = ps('password'); $pw2 = ps('password2');
|
||||
if (strlen($un) < 3) $errors[] = 'Username must be at least 3 characters.';
|
||||
if (!preg_match('/^[a-zA-Z0-9_]+$/',$un)) $errors[] = 'Username: letters, numbers, underscores only.';
|
||||
if ($em && !filter_var($em, FILTER_VALIDATE_EMAIL)) $errors[] = 'Invalid email address.';
|
||||
if (strlen($pw) < 6) $errors[] = 'Password must be at least 6 characters.';
|
||||
if ($pw !== $pw2) $errors[] = 'Passwords do not match.';
|
||||
if (!$errors && qval("SELECT id FROM users WHERE username=?",[$un])) $errors[] = 'Username already taken.';
|
||||
if (!$errors && $em && qval("SELECT id FROM users WHERE email=?",[$em])) $errors[] = 'Email already registered.';
|
||||
if (!$errors) {
|
||||
$id = qrun("INSERT INTO users(username,email,password)VALUES(?,?,?)",[$un,$em?:null,password_hash($pw,PASSWORD_BCRYPT)]);
|
||||
loginUser(qone("SELECT * FROM users WHERE id=?",[$id]));
|
||||
flash('Welcome to Discover Keyser WV!','success'); go('/index.php');
|
||||
}
|
||||
}
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="form-box">
|
||||
<h1 class="form-title">Create Account</h1>
|
||||
<p class="form-sub">Join the Discover Keyser WV community</p>
|
||||
<?php if ($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<div class="fg"><label class="flabel">USERNAME *</label><input type="text" name="username" class="finput" value="<?=e($_POST['username']??'')?>" autocomplete="username" required autofocus><div class="fhint">Letters, numbers, underscores. Minimum 3 characters.</div></div>
|
||||
<div class="fg"><label class="flabel">EMAIL (optional)</label><input type="email" name="email" class="finput" value="<?=e($_POST['email']??'')?>" autocomplete="email"></div>
|
||||
<div class="fg"><label class="flabel">PASSWORD *</label><input type="password" name="password" class="finput" autocomplete="new-password" required><div class="fhint">Minimum 6 characters.</div></div>
|
||||
<div class="fg"><label class="flabel">CONFIRM PASSWORD *</label><input type="password" name="password2" class="finput" autocomplete="new-password" required></div>
|
||||
<button type="submit" class="btn btn-primary btn-block" style="margin-top:.5rem">Create Account</button>
|
||||
</form>
|
||||
<p style="text-align:center;margin-top:1.25rem;font-size:.88rem;color:var(--text-m)">Already have an account? <a href="/login.php">Sign in</a></p>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$slug = gs('slug');
|
||||
|
||||
$b = qone("
|
||||
SELECT
|
||||
b.id, b.name, b.slug, b.subcategory, b.description,
|
||||
b.address, b.phone, b.email, b.website,
|
||||
COALESCE(b.hours,'{}') AS hours,
|
||||
b.lat, b.lng, b.is_active, b.is_featured, b.created_at,
|
||||
b.category_id,
|
||||
COALESCE(c.name,'Uncategorized') AS cat,
|
||||
COALESCE(c.icon,'🏢') AS cat_icon,
|
||||
COALESCE(AVG(CASE WHEN r.is_approved=1 THEN r.rating END), 0) AS avg,
|
||||
COUNT(CASE WHEN r.is_approved=1 THEN r.id END) AS rcnt
|
||||
FROM businesses b
|
||||
LEFT JOIN categories c ON c.id = b.category_id
|
||||
LEFT JOIN reviews r ON r.business_id = b.id
|
||||
WHERE b.slug = ? AND b.is_active = 1
|
||||
GROUP BY b.id
|
||||
", [$slug]);
|
||||
|
||||
if (!$b) { flash('Business not found.','error'); go('/directory.php'); }
|
||||
|
||||
/* ── POST handlers ─────────────────────────────────────── */
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'review' && authed()) {
|
||||
$rating = max(1, min(5, (int)p('rating', 5)));
|
||||
$title = ps('rtitle');
|
||||
$body = ps('rbody');
|
||||
$ex = qval("SELECT id FROM reviews WHERE business_id=? AND user_id=?", [$b['id'], uid()]);
|
||||
if ($ex) {
|
||||
qrun("UPDATE reviews SET rating=?,title=?,body=?,created_at=datetime('now') WHERE id=?",
|
||||
[$rating, $title, $body, $ex]);
|
||||
} else {
|
||||
qrun("INSERT INTO reviews(business_id,user_id,rating,title,body)VALUES(?,?,?,?,?)",
|
||||
[$b['id'], uid(), $rating, $title, $body]);
|
||||
}
|
||||
flash('Review saved!', 'success');
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'post_alert' && owns($b['id'])) {
|
||||
$t = ps('atitle');
|
||||
$body = ps('abody');
|
||||
$type = p('atype', 'info');
|
||||
if ($t && $body) {
|
||||
qrun("INSERT INTO alerts(business_id,type,title,body)VALUES(?,?,?,?)",
|
||||
[$b['id'], $type, $t, $body]);
|
||||
flash('Alert posted!', 'success');
|
||||
}
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'del_alert' && owns($b['id'])) {
|
||||
qrun("DELETE FROM alerts WHERE id=? AND business_id=?", [(int)p('aid'), $b['id']]);
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'del_review' && isAdmin()) {
|
||||
qrun("DELETE FROM reviews WHERE id=?", [(int)p('rid')]);
|
||||
flash('Review deleted.', 'success');
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'report' && authed()) {
|
||||
$rtype = p('rtype', 'correction');
|
||||
$rmsg = ps('rmsg');
|
||||
if ($rmsg) {
|
||||
qrun("INSERT INTO reports(business_id,user_id,type,message)VALUES(?,?,?,?)",
|
||||
[$b['id'], uid(), $rtype, $rmsg]);
|
||||
flash('Report submitted. Thank you!', 'success');
|
||||
}
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Page data ─────────────────────────────────────────── */
|
||||
$pageTitle = e($b['name']) . ' — Keyser, WV';
|
||||
$activeNav = 'dir';
|
||||
|
||||
$reviews = qall("
|
||||
SELECT r.*, u.username
|
||||
FROM reviews r
|
||||
JOIN users u ON u.id = r.user_id
|
||||
WHERE r.business_id = ? AND r.is_approved = 1
|
||||
ORDER BY r.created_at DESC
|
||||
", [$b['id']]);
|
||||
|
||||
$sects = qall("SELECT * FROM menu_sections WHERE business_id=? ORDER BY sort_order", [$b['id']]);
|
||||
$mitems = [];
|
||||
foreach ($sects as $s) {
|
||||
$mitems[$s['id']] = qall(
|
||||
"SELECT * FROM menu_items WHERE section_id=? AND is_available=1 ORDER BY sort_order",
|
||||
[$s['id']]
|
||||
);
|
||||
}
|
||||
|
||||
$alerts = qall("SELECT * FROM alerts WHERE business_id=? AND is_active=1 ORDER BY created_at DESC", [$b['id']]);
|
||||
$myrev = authed() ? qone("SELECT * FROM reviews WHERE business_id=? AND user_id=?", [$b['id'], uid()]) : null;
|
||||
$hours = parseHours($b['hours']);
|
||||
$isOwner = owns($b['id']);
|
||||
$cat = (string)($b['cat'] ?? 'Uncategorized');
|
||||
$catIcon = (string)($b['cat_icon'] ?? '🏢');
|
||||
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="detail-hdr">
|
||||
<div class="detail-hdr-inner">
|
||||
<div class="breadcrumb">
|
||||
<a href="/index.php">Home</a><span>›</span>
|
||||
<a href="/directory.php">Directory</a><span>›</span>
|
||||
<a href="/directory.php?cat=<?=urlencode($cat)?>"><?=e($catIcon.' '.$cat)?></a><span>›</span>
|
||||
<?=e($b['name'])?>
|
||||
</div>
|
||||
<div class="cat-badge"><?=e($catIcon.' '.$cat)?><?= $b['subcategory'] ? ' · '.e($b['subcategory']) : '' ?></div>
|
||||
<h1 style="font-size:clamp(1.9rem,4vw,2.9rem);margin:.4rem 0"><?=e($b['name'])?></h1>
|
||||
<div class="flex-row" style="margin-top:.6rem;gap:.9rem">
|
||||
<?=starDisplay((float)($b['avg'] ?? 0))?>
|
||||
<span class="rat-score"><?=number_format((float)($b['avg'] ?? 0), 1)?></span>
|
||||
<span class="rat-cnt">(<?=(int)($b['rcnt'] ?? 0)?> review<?=(int)($b['rcnt'] ?? 0) != 1 ? 's' : ''?>)</span>
|
||||
<?php if ($b['is_featured']): ?><span class="featured-badge">★ FEATURED</span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-body">
|
||||
<div><!-- main col -->
|
||||
|
||||
<?php if ($alerts): ?>
|
||||
<div style="margin-bottom:1.75rem">
|
||||
<div class="eyebrow" style="margin-bottom:.6rem">📢 BUSINESS UPDATES</div>
|
||||
<?php foreach ($alerts as $a): ?>
|
||||
<div class="alert-item a-<?=e($a['type'] ?? 'info')?>">
|
||||
<div class="alert-ico"><?=alertIcon($a['type'] ?? '')?></div>
|
||||
<div style="flex:1">
|
||||
<div class="alert-title"><?=e($a['title'] ?? '')?></div>
|
||||
<div class="alert-body"><?=e($a['body'] ?? '')?></div>
|
||||
<div class="alert-meta"><?=ago($a['created_at'] ?? '')?></div>
|
||||
</div>
|
||||
<?php if ($isOwner): ?>
|
||||
<form method="POST" style="margin-left:.5rem">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="del_alert">
|
||||
<input type="hidden" name="aid" value="<?=(int)$a['id']?>">
|
||||
<button class="btn btn-xs btn-danger" data-confirm="Delete this alert?">✕</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isOwner): ?>
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:1.75rem">
|
||||
<div class="ibox-title">📢 POST BUSINESS ALERT</div>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="post_alert">
|
||||
<div class="form-row" style="margin-bottom:.7rem">
|
||||
<input type="text" name="atitle" class="finput" placeholder="Alert title" required>
|
||||
<select name="atype" class="fselect">
|
||||
<option value="info">ℹ️ Info</option>
|
||||
<option value="news">📰 News</option>
|
||||
<option value="event">🎉 Event</option>
|
||||
<option value="warning">⚠️ Warning</option>
|
||||
<option value="sale">🏷️ Sale / Promo</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea name="abody" class="ftextarea" style="min-height:65px" placeholder="Alert details…" required></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top:.65rem">Post Alert</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="ibox" style="margin-bottom:1.75rem">
|
||||
<div class="ibox-title">ABOUT <?=strtoupper(e($b['name']))?></div>
|
||||
<p style="color:var(--text-m);line-height:1.85"><?=e($b['description'] ?? '')?></p>
|
||||
</div>
|
||||
|
||||
<?php if ($sects): ?>
|
||||
<div class="ibox" style="margin-bottom:1.75rem">
|
||||
<div class="ibox-title">🍽️ MENU</div>
|
||||
<?php foreach ($sects as $s): ?>
|
||||
<div class="menu-sec">
|
||||
<div class="menu-sec-name"><?=e($s['name'] ?? '')?></div>
|
||||
<?php if (!empty($s['description'])): ?>
|
||||
<div class="menu-sec-desc"><?=e($s['description'])?></div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($mitems[$s['id']] as $mi): ?>
|
||||
<div class="menu-item">
|
||||
<div class="mi-info">
|
||||
<div class="mi-name"><?=e($mi['name'] ?? '')?></div>
|
||||
<?php if (!empty($mi['description'])): ?>
|
||||
<div class="mi-desc"><?=e($mi['description'])?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ((float)($mi['price'] ?? 0) > 0): ?>
|
||||
<div class="mi-price"><?=money((float)$mi['price'])?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($isOwner): ?>
|
||||
<a href="/menu.php?id=<?=(int)$b['id']?>" class="btn btn-outline btn-sm">✏️ Manage Menu</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif ($isOwner): ?>
|
||||
<div style="margin-bottom:1.75rem">
|
||||
<a href="/menu.php?id=<?=(int)$b['id']?>" class="btn btn-outline">+ Add Menu</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3 style="font-family:'Oswald',sans-serif;letter-spacing:.1em;font-size:1.05rem;color:var(--text);margin-bottom:1.1rem;padding-bottom:.6rem;border-bottom:1px solid var(--bdr)">
|
||||
⭐ REVIEWS (<?=(int)($b['rcnt'] ?? 0)?>)
|
||||
</h3>
|
||||
|
||||
<?php if (authed()): ?>
|
||||
<div class="ibox" style="margin-bottom:1.5rem">
|
||||
<div class="ibox-title"><?= $myrev ? 'UPDATE YOUR REVIEW' : 'WRITE A REVIEW' ?></div>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="review">
|
||||
<div class="fg">
|
||||
<label class="flabel">YOUR RATING</label>
|
||||
<?=starPicker('rating', (int)($myrev['rating'] ?? 0))?>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">TITLE (optional)</label>
|
||||
<input type="text" name="rtitle" class="finput"
|
||||
value="<?=e($myrev['title'] ?? '')?>" placeholder="One-line summary…">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">YOUR REVIEW</label>
|
||||
<textarea name="rbody" class="ftextarea" placeholder="Share your experience…"><?=e($myrev['body'] ?? '')?></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Submit Review</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="background:var(--bg4);border:1px solid var(--bdr);border-radius:6px;padding:1.1rem;text-align:center;margin-bottom:1.4rem">
|
||||
<a href="/login.php" class="btn btn-outline btn-sm">Login to Write a Review</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($reviews as $r): ?>
|
||||
<div class="rev-card">
|
||||
<div class="rev-hdr">
|
||||
<div>
|
||||
<div class="rev-author"><?=e($r['username'] ?? 'Anonymous')?></div>
|
||||
<?=starDisplay((float)($r['rating'] ?? 0))?>
|
||||
</div>
|
||||
<div class="flex-row" style="gap:.5rem">
|
||||
<span class="rev-date"><?=ago($r['created_at'] ?? '')?></span>
|
||||
<?php if (isAdmin()): ?>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="del_review">
|
||||
<input type="hidden" name="rid" value="<?=(int)$r['id']?>">
|
||||
<button class="btn btn-xs btn-danger" data-confirm="Delete this review?">Delete</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($r['title'])): ?><div class="rev-title"><?=e($r['title'])?></div><?php endif; ?>
|
||||
<?php if (!empty($r['body'])): ?><div class="rev-body"><?=e($r['body'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (!$reviews): ?>
|
||||
<p style="color:var(--text-d);font-size:.88rem">No reviews yet — be the first!</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<?php if (authed()): ?>
|
||||
<details>
|
||||
<summary style="cursor:pointer;font-size:.82rem;color:var(--text-d);font-family:'Oswald',sans-serif;letter-spacing:.06em">
|
||||
🚩 REPORT A LISTING ISSUE
|
||||
</summary>
|
||||
<div class="ibox" style="margin-top:.75rem">
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="report">
|
||||
<div class="fg">
|
||||
<label class="flabel">ISSUE TYPE</label>
|
||||
<select name="rtype" class="fselect">
|
||||
<option value="correction">Incorrect Information</option>
|
||||
<option value="closed">Business Closed / Moved</option>
|
||||
<option value="hours">Wrong Hours</option>
|
||||
<option value="phone">Wrong Phone Number</option>
|
||||
<option value="other">Other Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">DESCRIBE THE ISSUE</label>
|
||||
<textarea name="rmsg" class="ftextarea" style="min-height:75px" required
|
||||
placeholder="Please describe what needs correction…"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Submit Report</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<?php else: ?>
|
||||
<p style="font-size:.82rem;color:var(--text-d)">
|
||||
🚩 <a href="/login.php">Log in</a> to report a listing issue.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /main col -->
|
||||
|
||||
<div><!-- sidebar -->
|
||||
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">BUSINESS INFO</div>
|
||||
<?php if (!empty($b['address'])): ?>
|
||||
<div class="irow"><span class="irow-i">📍</span><span class="irow-v"><?=e($b['address'])?></span></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($b['phone'])): ?>
|
||||
<div class="irow"><span class="irow-i">📞</span>
|
||||
<a href="tel:<?=e($b['phone'])?>" class="irow-v" style="color:var(--gold)"><?=e($b['phone'])?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($b['email'])): ?>
|
||||
<div class="irow"><span class="irow-i">✉️</span>
|
||||
<a href="mailto:<?=e($b['email'])?>" class="irow-v" style="color:var(--gold)"><?=e($b['email'])?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($b['website'])): ?>
|
||||
<div class="irow"><span class="irow-i">🌐</span>
|
||||
<a href="https://<?=e($b['website'])?>" target="_blank" rel="noopener"
|
||||
class="irow-v" style="color:var(--gold);word-break:break-all"><?=e($b['website'])?> ↗</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="irow">
|
||||
<span class="irow-i">🏷️</span>
|
||||
<span class="irow-v"><?=e($cat)?><?= !empty($b['subcategory']) ? ' · '.e($b['subcategory']) : '' ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($hours): ?>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">🕐 HOURS OF OPERATION</div>
|
||||
<div class="hrs-grid">
|
||||
<?php foreach ($hours as $day => $time): ?>
|
||||
<div class="hrs-row">
|
||||
<span class="hrs-day"><?=strtoupper(substr(e((string)$day), 0, 3))?></span>
|
||||
<span class="hrs-time"><?=e((string)$time)?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($b['address'])): ?>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">📍 LOCATION</div>
|
||||
<div style="background:var(--bg4);border:1px dashed var(--bdr);border-radius:4px;padding:1.75rem;text-align:center">
|
||||
<p style="font-size:.84rem;color:var(--text-m);margin-bottom:.75rem"><?=e($b['address'])?></p>
|
||||
<a href="https://maps.google.com/?q=<?=urlencode($b['name'].' '.$b['address'])?>"
|
||||
target="_blank" rel="noopener" class="btn btn-outline btn-sm">Open in Google Maps ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isOwner): ?>
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">⚙️ OWNER CONTROLS</div>
|
||||
<a href="/menu.php?id=<?=(int)$b['id']?>" class="btn btn-outline btn-sm btn-block"
|
||||
style="margin-bottom:.5rem">🍽️ Manage Menu</a>
|
||||
<?php if (isAdmin()): ?>
|
||||
<a href="/admin/biz_edit.php?id=<?=(int)$b['id']?>" class="btn btn-secondary btn-sm btn-block">
|
||||
✏️ Edit Listing
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /sidebar -->
|
||||
</div><!-- /detail-body -->
|
||||
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,157 @@
|
||||
# Discover Keyser WV — Tourism Website
|
||||
### PHP/SQLite City Tourism Site for Keyser, West Virginia
|
||||
|
||||
---
|
||||
|
||||
## QUICK START
|
||||
|
||||
1. **Extract** this zip to your web server's document root (or any folder)
|
||||
2. **Make the `data/` directory writable:**
|
||||
```
|
||||
chmod 777 data/
|
||||
```
|
||||
3. **Visit the site** — the database is created automatically on first load
|
||||
4. **Admin login:**
|
||||
- URL: `/admin/`
|
||||
- Username: `administrator`
|
||||
- Password: `password123`
|
||||
⚠️ **Change the admin password immediately** via Admin → Settings
|
||||
|
||||
---
|
||||
|
||||
## REQUIREMENTS
|
||||
|
||||
- **PHP** 8.0 or higher
|
||||
- **PDO SQLite** extension (enabled by default on most hosts)
|
||||
- **Web server:** Apache, Nginx, or `php -S` for local dev
|
||||
|
||||
### Local Development (no web server needed):
|
||||
```bash
|
||||
cd /path/to/keyser-wv
|
||||
php -S localhost:8080
|
||||
# Then visit: http://localhost:8080
|
||||
```
|
||||
|
||||
### Apache / cPanel / Shared Hosting:
|
||||
- Upload all files to `public_html/` or your domain folder
|
||||
- Ensure `data/` directory has write permission (chmod 777 or 755)
|
||||
- No `.htaccess` needed — plain PHP files
|
||||
|
||||
---
|
||||
|
||||
## FEATURES
|
||||
|
||||
### Public Site
|
||||
- 🏠 **Homepage** — hero banner, business stats, alerts feed, featured listings, upcoming events
|
||||
- 📋 **Business Directory** — searchable/filterable grid of 60+ real Keyser WV businesses
|
||||
- 🏢 **Business Detail Pages** — full info, hours, menus, reviews, alerts, Google Maps link
|
||||
- ⭐ **Reviews** — star ratings and written reviews (login required)
|
||||
- 🍽️ **Menus** — full menu display with sections, items, descriptions, prices
|
||||
- 📢 **Alerts/News** — business owners can post announcements
|
||||
- 🗺️ **Attractions** — 15+ attractions in Keyser & Mineral County
|
||||
- 📅 **Events Calendar** — community event submission and display
|
||||
- 📖 **About Page** — rich Keyser WV history, timeline, famous people, geography
|
||||
- 🚩 **Report Button** — logged-in users can report listing errors
|
||||
|
||||
### User Accounts
|
||||
- Sign up with username & password
|
||||
- Login / Logout
|
||||
- Account settings (email, password)
|
||||
- Owner dashboard to manage assigned businesses
|
||||
- Submit events
|
||||
|
||||
### Admin Panel (`/admin/`)
|
||||
- **Dashboard** — stats, pending events, open reports
|
||||
- **Businesses** — full CRUD: add/edit/delete, toggle active/featured, set hours
|
||||
- **Categories** — manage business categories with emoji icons
|
||||
- **Menus** — manage menu sections and items with prices for any business
|
||||
- **Reviews** — approve, hide, or delete any review
|
||||
- **Alerts** — post/manage business announcements
|
||||
- **Attractions** — full CRUD for attractions
|
||||
- **Events** — approve/reject submissions, add events, feature events
|
||||
- **Users** — add/edit/delete users, reset passwords, suspend accounts
|
||||
- **Assign Owners** — link users to businesses for self-management
|
||||
- **Reports** — review and close user-submitted listing corrections
|
||||
- **Settings** — toggle registration on/off, event approval, change admin password
|
||||
|
||||
---
|
||||
|
||||
## FILE STRUCTURE
|
||||
|
||||
```
|
||||
keyser-wv/
|
||||
├── index.php # Homepage
|
||||
├── directory.php # Business directory + search
|
||||
├── business.php # Business detail page
|
||||
├── menu.php # Owner menu manager
|
||||
├── attractions.php # Attractions page
|
||||
├── events.php # Events calendar + submission
|
||||
├── about.php # About Keyser WV history
|
||||
├── login.php # Login
|
||||
├── register.php # Registration
|
||||
├── logout.php # Logout
|
||||
├── account.php # Account settings
|
||||
├── dashboard.php # User/owner dashboard
|
||||
├── includes/
|
||||
│ ├── boot.php # Bootstrap (session + DB init)
|
||||
│ ├── db.php # Database, schema, seed data
|
||||
│ ├── functions.php # Auth, helpers, CSRF, stars
|
||||
│ ├── header.php # HTML header + navbar
|
||||
│ └── footer.php # Footer + JS
|
||||
├── admin/
|
||||
│ ├── index.php # Admin dashboard
|
||||
│ ├── businesses.php # Manage businesses
|
||||
│ ├── categories.php # Manage categories
|
||||
│ ├── menus.php # Manage all menus
|
||||
│ ├── reviews.php # Manage reviews
|
||||
│ ├── alerts.php # Manage alerts
|
||||
│ ├── attractions.php # Manage attractions
|
||||
│ ├── events.php # Manage events
|
||||
│ ├── users.php # Manage users
|
||||
│ ├── owners.php # Assign owners to businesses
|
||||
│ ├── reports.php # Handle listing reports
|
||||
│ └── settings.php # Site settings + password
|
||||
├── assets/
|
||||
│ ├── css/style.css # Complete dark WV theme
|
||||
│ └── js/app.js # Nav, dropdowns, confirm dialogs
|
||||
└── data/
|
||||
└── keyser.db # SQLite database (auto-created)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PRE-LOADED DATA
|
||||
|
||||
The database is seeded on first run with real Keyser WV data:
|
||||
|
||||
- **60+ businesses** across 13 categories (dining, retail, healthcare, education, etc.)
|
||||
- **Full menus** for Castiglia's Italian Eatery, The Candlewyck Inn, and Queens Point Coffee
|
||||
- **6 sample alerts** from real local businesses
|
||||
- **15 attractions** in Keyser & Mineral County
|
||||
- **7 events** including Mineral County Fair, Christmas Parade, PSC Athletics
|
||||
- **1 admin account** (administrator / password123)
|
||||
|
||||
---
|
||||
|
||||
## DESIGN
|
||||
|
||||
- **Dark theme** with West Virginia colors: Gold (#c9a84c) + Navy (#1a3a5c) on near-black
|
||||
- **Fonts:** Playfair Display (headings), Oswald (labels), Source Sans 3 (body)
|
||||
- **Fully responsive** — mobile, tablet, desktop
|
||||
- **Animated hero** with mountain silhouette and wind turbines
|
||||
- **No external dependencies** — pure PHP, SQLite, vanilla CSS/JS
|
||||
|
||||
---
|
||||
|
||||
## SECURITY
|
||||
|
||||
- CSRF tokens on all POST forms
|
||||
- Password hashing with bcrypt
|
||||
- HTML output escaped with `htmlspecialchars()`
|
||||
- Admin pages require `is_admin = 1`
|
||||
- Session regeneration on login/logout
|
||||
- Prepared statements everywhere (SQL injection prevention)
|
||||
|
||||
---
|
||||
|
||||
*Built for Keyser, West Virginia — The Friendliest City in the U.S.A.*
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$activeNav='about';
|
||||
$pageTitle='About Keyser, WV — History, Facts & Heritage';
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner">
|
||||
<div class="eyebrow">DISCOVER OUR STORY</div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin-bottom:.4rem">About Keyser, West Virginia</h1>
|
||||
<p style="color:var(--text-m)">County seat of Mineral County — where Appalachian heritage meets the North Branch Potomac River.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<!-- Quick Facts -->
|
||||
<div class="eyebrow" style="margin-bottom:1rem">QUICK FACTS</div>
|
||||
<div class="fact-grid" style="margin-bottom:3.5rem">
|
||||
<?php foreach([
|
||||
['📍','County Seat','Mineral County, WV — county created February 1, 1866'],
|
||||
['📅','Incorporated','1874 — named for B&O Railroad VP William Keyser'],
|
||||
['🏔️','Elevation','809 feet (246 m) above sea level'],
|
||||
['👥','Population','~4,853 city (2020); Mineral County ~26,938'],
|
||||
['🌊','Location','Where New Creek meets the North Branch Potomac River'],
|
||||
['📏','County Area','329.3 square miles (WV\'s 5th smallest county)'],
|
||||
['🚂','Named For','William Keyser, VP of the Baltimore & Ohio Railroad'],
|
||||
['🎓','Higher Ed','Potomac State College of WVU (est. 1901 on Civil War Fort Fuller)'],
|
||||
['🏥','Healthcare','WVU Medicine Potomac Valley Hospital — full-service regional hospital'],
|
||||
['💨','Wind Energy','132 turbines at NedPower Mount Storm Wind Farm on Allegheny Front'],
|
||||
['🏛️','Historic','Multiple sites on the National Register of Historic Places'],
|
||||
['✈️','Airport','Greater Cumberland Regional Airport (CBE) in Wiley Ford — minutes away'],
|
||||
['🌲','Heritage','Part of the Appalachian Forest National Heritage Area'],
|
||||
['🏈','Sports','Home of the WVU Potomac State Catamounts'],
|
||||
] as [$ic,$l,$v]): ?>
|
||||
<div class="fact-card"><div class="fact-icon"><?=$ic?></div><div class="fact-lbl"><?=e($l)?></div><div class="fact-val"><?=e($v)?></div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- History & Timeline -->
|
||||
<div class="g2" style="margin-bottom:3.5rem;align-items:start">
|
||||
<div>
|
||||
<div class="eyebrow">OUR HISTORY</div>
|
||||
<h2 class="sec-title" style="margin-bottom:1.25rem">From Paddy Town to Keyser</h2>
|
||||
<div class="prose">
|
||||
<p><strong>Ancient roots:</strong> For thousands of years indigenous peoples — including the Adena culture (1000 BC–200 AD) — inhabited the rich valleys along Mineral County's rivers. The area was later traversed by the Haudenosaunee (Iroquois) confederation in search of hunting grounds.</p>
|
||||
<p><strong>Patrick McCarty and "Paddy Town":</strong> In the mid-1700s, Irish immigrant Patrick McCarty of County Tyrone established a homestead near the confluence of New Creek and the North Branch Potomac. The settlement became "Paddy Town" — later New Creek.</p>
|
||||
<p><strong>George Washington on the frontier:</strong> As a young surveyor and colonial military officer during the French and Indian War (1750s), George Washington traversed and helped defend the Eastern Panhandle region, ordering stockades built near present-day Mineral County.</p>
|
||||
<p><strong>The Baltimore & Ohio Railroad transforms everything (1842):</strong> The arrival of the B&O main line in 1842 was the defining moment in Mineral County's development. New Creek became a critical rail junction — coal, timber, and agricultural products flowed east while settlers and commerce moved west.</p>
|
||||
<p><strong>Civil War (1861–1865):</strong> Located in the Eastern Panhandle, the area saw repeated skirmishes as the town changed hands. New Creek served as a Union Army training camp for troops from Pennsylvania, Ohio, Indiana, and Illinois. <strong>Fort Fuller</strong> on Fort Hill was the most significant fortification.</p>
|
||||
<p><strong>Fort Fuller — Two future legends:</strong> The Civil War fort on what is now Potomac State College's campus was commanded at different times by two remarkable figures: future 23rd U.S. President <strong>Benjamin Harrison</strong>, and <strong>Lew Wallace</strong> — who later wrote the epic novel <em>Ben-Hur: A Tale of the Christ</em> (1880). The fort was also called Fort Kelly.</p>
|
||||
<p><strong>Mineral County created (February 1, 1866):</strong> Carved from Hampshire County. The county seat was located at New Creek as a compromise between Elk Garden and Piedmont. The town was officially renamed <strong>Keyser</strong> in honor of William Keyser, Vice President of the B&O Railroad.</p>
|
||||
<p><strong>Keyser incorporated (1874):</strong> The city was formally incorporated, establishing the civic institutions that anchor the community today.</p>
|
||||
<p><strong>Potomac State College (1901):</strong> The Keyser Preparatory School opened on historic Fort Hill, later joining the WVU system as Potomac State College. Today it serves ~1,200–1,800 students in 40+ programs and is a major economic and cultural anchor for Keyser.</p>
|
||||
<p><strong>The Lincoln connection — Nancy Hanks:</strong> Near Doll's Gap just outside Keyser is believed to be the birthplace of <strong>Nancy Hanks</strong>, mother of President Abraham Lincoln. This extraordinary connection to America's most celebrated president is part of Mineral County's remarkable presidential heritage.</p>
|
||||
<p><strong>Henry Louis Gates Jr.:</strong> The nearby Mineral County community of Piedmont is the hometown of <strong>Henry Louis Gates Jr.</strong> — the Harvard scholar, author, and host of <em>Finding Your Roots</em>. Gates has written movingly about growing up along the North Branch of the Potomac.</p>
|
||||
<p><strong>Keyser today:</strong> The city continues as a regional hub for education, healthcare, retail, and government services, anchored by WVU Medicine Potomac Valley Hospital, Potomac State College, and a vibrant Main Street. Tourism is growing with the expansion of outdoor recreation on Jennings Randolph Lake and the North Branch Potomac.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="eyebrow" style="margin-bottom:1rem">HISTORICAL TIMELINE</div>
|
||||
<div class="timeline">
|
||||
<?php foreach([
|
||||
['~750 BC–200 AD','Adena culture inhabits Mineral County valleys'],
|
||||
['Mid-1700s','Patrick McCarty settles area — "Paddy Town" established'],
|
||||
['1750s','George Washington defends the Eastern Panhandle frontier'],
|
||||
['1755','French and Indian War — colonial forts ordered in present Mineral County'],
|
||||
['1842','B&O Railroad main line arrives — transforms New Creek forever'],
|
||||
['1861–65','Civil War — Union Army training camp at New Creek; Fort Fuller on Fort Hill'],
|
||||
['1864','Benjamin Harrison & Lew Wallace command Fort Fuller'],
|
||||
['1866','Mineral County created from Hampshire County (Feb. 1); town renamed Keyser'],
|
||||
['1874','Keyser formally incorporated as a city'],
|
||||
['1901','Keyser Preparatory School opens — becomes Potomac State College'],
|
||||
['1930s–50s','Coal and railroad economy sustains Keyser through Depression and WWII'],
|
||||
['1972','Jennings Randolph Dam completed; 952-acre reservoir fills'],
|
||||
['1990s','WVU Potomac State College joins WVU system fully'],
|
||||
['2000s','NedPower Mount Storm Wind Farm — 132 turbines on Allegheny Front'],
|
||||
['2012','Mineral County Historical Society Museum opens'],
|
||||
['Present','Tourism, outdoor recreation, and healthcare lead economic growth'],
|
||||
] as [$yr,$evt]): ?>
|
||||
<div class="tl-item"><span class="tl-yr"><?=e($yr)?></span><span class="tl-evt"><?=e($evt)?></span></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notable People -->
|
||||
<div class="eyebrow" style="margin-bottom:1rem">NOTABLE CONNECTIONS</div>
|
||||
<h2 class="sec-title" style="margin-bottom:1.5rem">Famous Figures of Mineral County</h2>
|
||||
<div class="famous-grid" style="margin-bottom:3.5rem">
|
||||
<?php foreach([
|
||||
['Benjamin Harrison','1833–1901','23rd President of the United States. Commanded Union troops at Fort Fuller (Fort Hill) in what is now Keyser during the Civil War — standing watch on the very hill where Potomac State College stands today.'],
|
||||
['Lew Wallace','1827–1905','Union Army General, Governor of New Mexico Territory, U.S. Ambassador to the Ottoman Empire, and author of the epic 1880 novel Ben-Hur. Also commanded Fort Fuller on Fort Hill in Keyser.'],
|
||||
['Nancy Hanks','1784–1818','Mother of President Abraham Lincoln. Believed to have been born near Doll\'s Gap in Mineral County — a remarkable piece of American presidential heritage connecting Keyser to the Lincoln story.'],
|
||||
['Henry Louis Gates Jr.','1950–present','Harvard scholar, MacArthur Fellow, author, filmmaker, and host of Finding Your Roots (PBS). Grew up in Piedmont, Mineral County. Has written extensively about growing up along the North Branch Potomac.'],
|
||||
['Patrick McCarty','~1720–?','Irish immigrant from County Tyrone who established the earliest European settlement in present-day Keyser around the 1750s, giving it the original name "Paddy Town."'],
|
||||
['William Keyser','~1820–?','Vice President of the Baltimore & Ohio Railroad. The city of Keyser was renamed in his honor in 1866 in recognition of the transformative role the B&O Railroad played in Mineral County\'s development.'],
|
||||
] as [$n,$y,$b]): ?>
|
||||
<div class="famous-card"><div class="famous-name"><?=e($n)?></div><div class="famous-yrs"><?=e($y)?></div><div class="famous-bio"><?=e($b)?></div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Geography -->
|
||||
<div class="eyebrow" style="margin-bottom:1rem">GEOGRAPHY & NATURE</div>
|
||||
<h2 class="sec-title" style="margin-bottom:1.25rem">Where the Mountains Meet the River</h2>
|
||||
<div class="prose" style="max-width:840px;margin-bottom:3rem">
|
||||
<p>Keyser lies at the convergence of <strong>New Creek</strong> and the <strong>North Branch of the Potomac River</strong> in a valley framed by the rolling ridges of the Appalachian Mountains. From its valley floor at 809 feet, the surrounding terrain rises to the dramatic <strong>Allegheny Front</strong> — reaching over 3,000 feet elevation — just to the west.</p>
|
||||
<p>The North Branch Potomac River, which forms the West Virginia-Maryland border, flows northeast through Keyser. This river and its tributaries have shaped the city's history, economy, and character for three centuries — carrying B&O Railroad freight for 150 years and today offering world-class <strong>smallmouth bass fishing</strong>, kayaking, and birdwatching.</p>
|
||||
<p>On clear days from Keyser, the <strong>132 wind turbines</strong> of the NedPower Mount Storm Wind Farm are visible on the Allegheny Front — one of the largest wind energy installations east of the Mississippi River. The turbines have become part of Keyser's distinctive skyline.</p>
|
||||
<p>The spectacular <strong>Queens Point sandstone cliff</strong> — rising ~400 feet above the North Branch in McCoole, Maryland directly across from Keyser — is one of the most dramatic natural features visible from the city and a beloved local landmark and photographer's destination.</p>
|
||||
<p>Mineral County is part of the <strong>Appalachian Forest National Heritage Area</strong> — over 500 square miles of forested Appalachian landscape where Revolutionary War-era sites, Civil War battlegrounds, and pioneer homesteads dot the landscape alongside natural wonders.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
requireLogin();
|
||||
$pageTitle = 'Account Settings — Keyser WV';
|
||||
$u = qone("SELECT * FROM users WHERE id=?",[uid()]);
|
||||
$errors = [];
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$em = ps('email'); $pw = ps('password'); $pw2 = ps('password2');
|
||||
if ($em && !filter_var($em,FILTER_VALIDATE_EMAIL)) $errors[] = 'Invalid email address.';
|
||||
if ($pw) {
|
||||
if (strlen($pw) < 6) $errors[] = 'Password must be at least 6 characters.';
|
||||
if ($pw !== $pw2) $errors[] = 'Passwords do not match.';
|
||||
}
|
||||
if (!$errors && $em && $em !== $u['email'] && qval("SELECT id FROM users WHERE email=? AND id!=?",[$em,uid()])) $errors[] = 'Email already in use.';
|
||||
if (!$errors) {
|
||||
if ($pw) qrun("UPDATE users SET email=?,password=? WHERE id=?",[$em?:null,password_hash($pw,PASSWORD_BCRYPT),uid()]);
|
||||
else qrun("UPDATE users SET email=? WHERE id=?",[$em?:null,uid()]);
|
||||
flash('Account updated successfully!','success'); go('/account.php');
|
||||
}
|
||||
$u['email'] = $em;
|
||||
}
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="form-box" style="max-width:520px">
|
||||
<h1 class="form-title">Account Settings</h1>
|
||||
<p class="form-sub">Manage your Discover Keyser WV account</p>
|
||||
<div class="ibox" style="margin-bottom:1.5rem">
|
||||
<div class="ibox-title">ACCOUNT INFO</div>
|
||||
<div class="irow"><span class="irow-i">👤</span><span class="irow-v"><?=e($u['username'])?> <?=isAdmin()?'<span class="badge badge-gold">ADMIN</span>':''?></span></div>
|
||||
<div class="irow"><span class="irow-i">📅</span><span class="irow-v">Joined <?=fdate($u['created_at'])?></span></div>
|
||||
<?php if($u['last_login']): ?><div class="irow"><span class="irow-i">🕐</span><span class="irow-v">Last login: <?=ago($u['last_login'])?></span></div><?php endif; ?>
|
||||
</div>
|
||||
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<div class="fg"><label class="flabel">EMAIL ADDRESS</label><input type="email" name="email" class="finput" value="<?=e($u['email']??'')?>"></div>
|
||||
<hr class="divider" style="margin:1.25rem 0">
|
||||
<p style="font-family:'Oswald',sans-serif;font-size:.72rem;letter-spacing:.1em;color:var(--gold);margin-bottom:.85rem">CHANGE PASSWORD (leave blank to keep current)</p>
|
||||
<div class="fg"><label class="flabel">NEW PASSWORD</label><input type="password" name="password" class="finput" autocomplete="new-password"></div>
|
||||
<div class="fg"><label class="flabel">CONFIRM NEW PASSWORD</label><input type="password" name="password2" class="finput" autocomplete="new-password"></div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// Admin header — included by all admin pages after setting $adminTitle
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
requireAdmin();
|
||||
$pageTitle = ($adminTitle ?? 'Admin') . ' — Keyser WV Admin';
|
||||
include dirname(__DIR__).'/includes/header.php';
|
||||
?>
|
||||
<div class="admin-wrap">
|
||||
<aside class="admin-side">
|
||||
<div class="admin-side-title">ADMIN PANEL</div>
|
||||
<a class="asl" href="/admin/">📊 Dashboard</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">DIRECTORY</div>
|
||||
<a class="asl" href="/admin/businesses.php">🏢 Businesses</a>
|
||||
<a class="asl" href="/admin/categories.php">🏷️ Categories</a>
|
||||
<a class="asl" href="/admin/menus.php">🍽️ Menus</a>
|
||||
<a class="asl" href="/admin/reviews.php">⭐ Reviews</a>
|
||||
<a class="asl" href="/admin/alerts.php">📢 Alerts</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">COMMUNITY</div>
|
||||
<a class="asl" href="/admin/events.php">📅 Events</a>
|
||||
<a class="asl" href="/admin/attractions.php">🗺️ Attractions</a>
|
||||
<a class="asl" href="/admin/reports.php">🚩 Reports</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">USERS</div>
|
||||
<a class="asl" href="/admin/users.php">👥 Users</a>
|
||||
<a class="asl" href="/admin/owners.php">🔑 Assign Owners</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">SETTINGS</div>
|
||||
<a class="asl" href="/admin/settings.php">⚙️ Site Settings</a>
|
||||
<hr style="border-color:var(--bdr);margin:1rem 0">
|
||||
<a class="asl" href="/index.php">🏠 View Site</a>
|
||||
<a class="asl" href="/logout.php">🚪 Sign Out</a>
|
||||
</aside>
|
||||
<div class="admin-main">
|
||||
<?php
|
||||
// Helper: render admin section title
|
||||
function adminTitle(string $t, string $sub=''): void {
|
||||
echo '<div class="admin-sec-title">'.e($t).'</div>';
|
||||
if($sub) echo '<p style="color:var(--text-m);font-size:.88rem;margin-top:-.5rem;margin-bottom:1rem">'.e($sub).'</p>';
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
$adminTitle = 'Businesses';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
// Handle POST actions
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'add' || $act === 'edit') {
|
||||
$fields = [
|
||||
'name' => ps('name'),
|
||||
'slug' => ps('slug') ?: makeSlug(ps('name')),
|
||||
'category_id' => (int)p('category_id'),
|
||||
'subcategory' => ps('subcategory'),
|
||||
'description' => ps('description'),
|
||||
'address' => ps('address'),
|
||||
'phone' => ps('phone'),
|
||||
'email' => ps('email'),
|
||||
'website' => ps('website'),
|
||||
'lat' => (float)p('lat', 39.4364),
|
||||
'lng' => (float)p('lng', -78.9762),
|
||||
'is_active' => (int)p('is_active', 1),
|
||||
'is_featured' => (int)p('is_featured', 0),
|
||||
];
|
||||
// Build hours JSON
|
||||
$days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
$hrs = [];
|
||||
foreach ($days as $d) { $v = ps("hours_$d"); if ($v) $hrs[$d] = $v; }
|
||||
$fields['hours'] = json_encode($hrs);
|
||||
|
||||
if (!$fields['name']) { flash('Business name is required.','error'); go('/admin/businesses.php'); }
|
||||
|
||||
if ($act === 'add') {
|
||||
// Ensure unique slug
|
||||
$base = $fields['slug']; $i = 0;
|
||||
while (qval("SELECT id FROM businesses WHERE slug=?",[$fields['slug'].($i?"-$i":"")])) $i++;
|
||||
$fields['slug'] .= $i ? "-$i" : "";
|
||||
qrun("INSERT INTO businesses(name,slug,category_id,subcategory,description,address,phone,email,website,hours,lat,lng,is_active,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)",array_values($fields));
|
||||
flash('Business added!','success');
|
||||
} else {
|
||||
$id = (int)p('id');
|
||||
// Check slug uniqueness excluding self
|
||||
$base = $fields['slug']; $i = 0;
|
||||
while (qval("SELECT id FROM businesses WHERE slug=? AND id!=?",[$fields['slug'].($i?"-$i":""),$id])) $i++;
|
||||
$fields['slug'] .= $i ? "-$i" : "";
|
||||
qrun("UPDATE businesses SET name=?,slug=?,category_id=?,subcategory=?,description=?,address=?,phone=?,email=?,website=?,hours=?,lat=?,lng=?,is_active=?,is_featured=? WHERE id=?",array_merge(array_values($fields),[$id]));
|
||||
flash('Business updated!','success');
|
||||
}
|
||||
go('/admin/businesses.php');
|
||||
}
|
||||
|
||||
if ($act === 'delete') {
|
||||
$id = (int)p('id');
|
||||
qrun("DELETE FROM businesses WHERE id=?",[$id]);
|
||||
flash('Business deleted.','success');
|
||||
go('/admin/businesses.php');
|
||||
}
|
||||
if ($act === 'toggle') {
|
||||
$id = (int)p('id');
|
||||
qrun("UPDATE businesses SET is_active = 1 - is_active WHERE id=?",[$id]);
|
||||
go('/admin/businesses.php');
|
||||
}
|
||||
}
|
||||
|
||||
$editId = iget('edit');
|
||||
$editing = $editId ? qone("SELECT * FROM businesses WHERE id=?",[$editId]) : null;
|
||||
$cats = qall("SELECT * FROM categories ORDER BY sort_order");
|
||||
$q = gs('q'); $page = max(1,iget('page')); $perPage = 25;
|
||||
$where = $q ? "WHERE b.name LIKE ? OR b.address LIKE ?" : "";
|
||||
$params = $q ? ["%$q%","%$q%"] : [];
|
||||
$total = (int)qval("SELECT COUNT(*) FROM businesses b $where",$params);
|
||||
$pages = max(1,(int)ceil($total/$perPage));
|
||||
$page = min($page,$pages);
|
||||
$offset = ($page-1)*$perPage;
|
||||
$bizs = qall("SELECT b.*,c.name cat FROM businesses b LEFT JOIN categories c ON c.id=b.category_id $where ORDER BY b.name LIMIT $perPage OFFSET $offset",$params);
|
||||
$days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
?>
|
||||
<?php adminTitle($editing ? 'Edit Business: '.e($editing['name']) : 'Manage Businesses') ?>
|
||||
|
||||
<!-- Add/Edit Form -->
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title"><?=$editing ? '✏️ EDIT: '.strtoupper(e($editing['name'])) : '➕ ADD NEW BUSINESS'?></div>
|
||||
<?php $h = $editing ? parseHours($editing['hours']) : []; ?>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="<?=$editing?'edit':'add'?>">
|
||||
<?php if($editing): ?><input type="hidden" name="id" value="<?=$editing['id']?>"><?php endif; ?>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">BUSINESS NAME *</label><input type="text" name="name" class="finput" value="<?=e($editing['name']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">SLUG (auto-generated)</label><input type="text" name="slug" class="finput" value="<?=e($editing['slug']??'')?>" placeholder="auto-generated from name"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">CATEGORY</label><select name="category_id" class="fselect"><?php foreach($cats as $c): ?><option value="<?=$c['id']?>"<?=($editing['category_id']??0)==$c['id']?' selected':''?>><?=e($c['icon'].' '.$c['name'])?></option><?php endforeach; ?></select></div>
|
||||
<div class="fg"><label class="flabel">SUBCATEGORY</label><input type="text" name="subcategory" class="finput" value="<?=e($editing['subcategory']??'')?>" placeholder="e.g. Italian Restaurant"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><textarea name="description" class="ftextarea" style="min-height:90px"><?=e($editing['description']??'')?></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">ADDRESS</label><input type="text" name="address" class="finput" value="<?=e($editing['address']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">PHONE</label><input type="text" name="phone" class="finput" value="<?=e($editing['phone']??'')?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">EMAIL</label><input type="email" name="email" class="finput" value="<?=e($editing['email']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">WEBSITE</label><input type="text" name="website" class="finput" value="<?=e($editing['website']??'')?>" placeholder="example.com (no https://)"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">LATITUDE</label><input type="number" name="lat" class="finput" value="<?=e($editing['lat']??39.4364)?>" step="0.0001"></div>
|
||||
<div class="fg"><label class="flabel">LONGITUDE</label><input type="number" name="lng" class="finput" value="<?=e($editing['lng']??-78.9762)?>" step="0.0001"></div>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem">
|
||||
<label class="flabel" style="margin-bottom:.6rem">HOURS OF OPERATION</label>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.5rem">
|
||||
<?php foreach($days as $d): ?>
|
||||
<div style="display:flex;gap:.4rem;align-items:center">
|
||||
<span style="font-family:'Oswald',sans-serif;font-size:.72rem;color:var(--gold);width:28px;flex-shrink:0"><?=$d?></span>
|
||||
<input type="text" name="hours_<?=$d?>" class="finput" style="font-size:.8rem;padding:.4rem .6rem" value="<?=e($h[$d]??'')?>" placeholder="e.g. 9am–5pm">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:.75rem">
|
||||
<div class="fg"><label class="flabel">STATUS</label><select name="is_active" class="fselect"><option value="1"<?=($editing['is_active']??1)?'selected':''?>>Active</option><option value="0"<?=isset($editing)&&!$editing['is_active']?' selected':''?>>Inactive (hidden)</option></select></div>
|
||||
<div class="fg"><label class="flabel">FEATURED</label><select name="is_featured" class="fselect"><option value="0"<?=!($editing['is_featured']??0)?' selected':''?>>Not Featured</option><option value="1"<?=($editing['is_featured']??0)?' selected':''?>>★ Featured</option></select></div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary"><?=$editing?'Update Business':'Add Business'?></button>
|
||||
<?php if($editing): ?><a href="/admin/businesses.php" class="btn btn-secondary">Cancel</a><?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Businesses list -->
|
||||
<div class="admin-sec-title">ALL BUSINESSES (<?=$total?>)</div>
|
||||
<form method="GET" style="display:flex;gap:.5rem;margin-bottom:1rem">
|
||||
<input type="text" name="q" class="finput" style="max-width:300px" value="<?=e($q)?>" placeholder="Search businesses…">
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Search</button>
|
||||
<?php if($q): ?><a href="/admin/businesses.php" class="btn btn-outline btn-sm">Clear</a><?php endif; ?>
|
||||
</form>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>NAME</th><th>CATEGORY</th><th>PHONE</th><th>STATUS</th><th>FEATURED</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($bizs as $b): ?>
|
||||
<tr>
|
||||
<td><a href="/business.php?slug=<?=e($b['slug'])?>" class="td-name" target="_blank"><?=e($b['name'])?></a></td>
|
||||
<td><span style="font-size:.78rem;color:var(--text-d)"><?=e($b['cat']??'—')?></span></td>
|
||||
<td style="font-size:.8rem"><?=e($b['phone'])?></td>
|
||||
<td><?=$b['is_active']?'<span class="badge badge-green">Active</span>':'<span class="badge badge-gray">Inactive</span>'?></td>
|
||||
<td><?=$b['is_featured']?'<span class="badge badge-gold">★</span>':'—'?></td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<a href="/admin/businesses.php?edit=<?=$b['id']?>" class="btn btn-xs btn-outline">✏️ Edit</a>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="toggle"><input type="hidden" name="id" value="<?=$b['id']?>"><button class="btn btn-xs btn-secondary"><?=$b['is_active']?'Deactivate':'Activate'?></button></form>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$b['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Permanently delete '<?=e($b['name'])?>' and all its data? This cannot be undone.">🗑️ Delete</button></form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if($pages>1): ?>
|
||||
<div style="display:flex;gap:.5rem;margin-top:1rem;flex-wrap:wrap">
|
||||
<?php for($i=1;$i<=$pages;$i++): ?><a href="?<?=http_build_query(array_filter(['q'=>$q,'page'=>$i]))?>" class="btn btn-sm<?=$i===$page?' btn-primary':' btn-outline'?>"><?=$i?></a><?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
$adminTitle = 'Dashboard';
|
||||
include __DIR__.'/admin_header.php';
|
||||
$stats = [
|
||||
'Businesses' => qval("SELECT COUNT(*) FROM businesses WHERE is_active=1"),
|
||||
'Users' => qval("SELECT COUNT(*) FROM users"),
|
||||
'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'"),
|
||||
'Open Reports' => qval("SELECT COUNT(*) FROM reports WHERE status='open'"),
|
||||
'Attractions' => qval("SELECT COUNT(*) FROM attractions WHERE is_active=1"),
|
||||
'Active Alerts' => qval("SELECT COUNT(*) FROM alerts WHERE is_active=1"),
|
||||
];
|
||||
$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");
|
||||
?>
|
||||
<?php adminTitle('Site Overview','Welcome to the Discover Keyser WV administration panel.') ?>
|
||||
|
||||
<div class="stat-cards">
|
||||
<?php foreach($stats as $lbl=>$val): ?>
|
||||
<div class="stat-card"><div class="sc-n"><?=$val?></div><div class="sc-l"><?=e($lbl)?></div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="g2" style="gap:1.5rem;margin-bottom:2rem">
|
||||
<?php if($pendingEvts): ?>
|
||||
<div>
|
||||
<div class="admin-sec-title">⏳ PENDING EVENTS (<?=count($pendingEvts)?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>EVENT</th><th>SUBMITTED BY</th><th>DATE</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($pendingEvts as $ev): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($ev['title'])?></td>
|
||||
<td><?=e($ev['username']??'—')?></td>
|
||||
<td><?=fdate($ev['event_date'])?></td>
|
||||
<td><a href="/admin/events.php?edit=<?=$ev['id']?>" class="btn btn-xs btn-primary">Review</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($openReports): ?>
|
||||
<div>
|
||||
<div class="admin-sec-title">🚩 OPEN REPORTS (<?=count($openReports)?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>BUSINESS</th><th>TYPE</th><th>FROM</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($openReports as $r): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($r['biz'])?></td>
|
||||
<td><span class="badge badge-gold"><?=e($r['type'])?></span></td>
|
||||
<td><?=e($r['reporter'])?></td>
|
||||
<td><a href="/admin/reports.php" class="btn btn-xs btn-outline">View</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="g2">
|
||||
<div>
|
||||
<div class="admin-sec-title">🏢 RECENT BUSINESSES</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>NAME</th><th>STATUS</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($recentBiz as $b): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($b['name'])?></td>
|
||||
<td><?=$b['is_active']?'<span class="badge badge-green">Active</span>':'<span class="badge badge-gray">Inactive</span>'?></td>
|
||||
<td><a href="/admin/businesses.php?edit=<?=$b['id']?>" class="btn btn-xs btn-outline">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/admin/businesses.php" class="btn btn-outline btn-sm" style="margin-top:.75rem">Manage All Businesses</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="admin-sec-title">👥 RECENT USERS</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>USERNAME</th><th>JOINED</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($recentUsers as $u): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($u['username'])?><?=$u['is_admin']?' <span class="badge badge-gold">ADMIN</span>':''?></td>
|
||||
<td><?=fdate($u['created_at'])?></td>
|
||||
<td><a href="/admin/users.php?edit=<?=$u['id']?>" class="btn btn-xs btn-outline">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/admin/users.php" class="btn btn-outline btn-sm" style="margin-top:.75rem">Manage All Users</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,3 @@
|
||||
</div><!-- /.admin-main -->
|
||||
</div><!-- /.admin-wrap -->
|
||||
<?php include dirname(__DIR__).'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// Admin header — included by all admin pages after setting $adminTitle
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
requireAdmin();
|
||||
$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'");
|
||||
?>
|
||||
<div class="admin-wrap">
|
||||
<aside class="admin-side">
|
||||
<div class="admin-side-title">ADMIN PANEL</div>
|
||||
<a class="asl" href="/admin/">📊 Dashboard</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">DIRECTORY</div>
|
||||
<a class="asl" href="/admin/businesses.php">🏢 Businesses</a>
|
||||
<a class="asl" href="/admin/categories.php">🏷️ Categories</a>
|
||||
<a class="asl" href="/admin/menus.php">🍽️ Menus</a>
|
||||
<a class="asl" href="/admin/reviews.php">⭐ Reviews</a>
|
||||
<a class="asl" href="/admin/alerts.php">📢 Alerts</a>
|
||||
<a class="asl" href="/admin/biz_edits.php" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<span>✏️ Owner Edits</span>
|
||||
<?php if ($_pendingEdits): ?>
|
||||
<span style="background:var(--gold);color:var(--bg0);font-family:'Oswald',sans-serif;font-size:.65rem;padding:.1rem .42rem;border-radius:10px;font-weight:600"><?=$_pendingEdits?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">COMMUNITY</div>
|
||||
<a class="asl" href="/admin/events.php">📅 Events</a>
|
||||
<a class="asl" href="/admin/attractions.php">🗺️ Attractions</a>
|
||||
<a class="asl" href="/admin/reports.php">🚩 Reports</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">USERS</div>
|
||||
<a class="asl" href="/admin/users.php">👥 Users</a>
|
||||
<a class="asl" href="/admin/owners.php">🔑 Assign Owners</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">SETTINGS</div>
|
||||
<a class="asl" href="/admin/settings.php">⚙️ Site Settings</a>
|
||||
<hr style="border-color:var(--bdr);margin:1rem 0">
|
||||
<a class="asl" href="/index.php">🏠 View Site</a>
|
||||
<a class="asl" href="/logout.php">🚪 Sign Out</a>
|
||||
</aside>
|
||||
<div class="admin-main">
|
||||
<?php
|
||||
function adminTitle(string $t, string $sub=''): void {
|
||||
echo '<div class="admin-sec-title">'.e($t).'</div>';
|
||||
if($sub) echo '<p style="color:var(--text-m);font-size:.88rem;margin-top:-.5rem;margin-bottom:1rem">'.e($sub).'</p>';
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
$adminTitle = 'Business Alerts';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
if ($act === 'add') {
|
||||
$bid=(int)p('business_id');$title=ps('title');$body=ps('body');$type=p('type','info');
|
||||
if($bid&&$title) qrun("INSERT INTO alerts(business_id,type,title,body)VALUES(?,?,?,?)",[$bid,$type,$title,$body]);
|
||||
flash('Alert posted!','success');
|
||||
}
|
||||
if ($act === 'toggle') { qrun("UPDATE alerts SET is_active=1-is_active WHERE id=?",[(int)p('id')]); }
|
||||
if ($act === 'delete') { qrun("DELETE FROM alerts WHERE id=?",[(int)p('id')]); flash('Alert deleted.','success'); }
|
||||
go('/admin/alerts.php');
|
||||
}
|
||||
|
||||
$bizs = qall("SELECT id,name FROM businesses WHERE is_active=1 ORDER BY name");
|
||||
$alerts = qall("SELECT a.*,b.name biz FROM alerts a JOIN businesses b ON b.id=a.business_id ORDER BY a.created_at DESC");
|
||||
?>
|
||||
<?php adminTitle('Business Alerts & Announcements') ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title">➕ POST NEW ALERT</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">BUSINESS</label><select name="business_id" class="fselect" required><option value="">— Select —</option><?php foreach($bizs as $b): ?><option value="<?=$b['id']?>"><?=e($b['name'])?></option><?php endforeach; ?></select></div>
|
||||
<div class="fg"><label class="flabel">TYPE</label><select name="type" class="fselect"><option value="info">ℹ️ Info</option><option value="news">📰 News</option><option value="event">🎉 Event</option><option value="warning">⚠️ Warning</option><option value="sale">🏷️ Sale/Promo</option></select></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">TITLE *</label><input type="text" name="title" class="finput" required></div>
|
||||
<div class="fg"><label class="flabel">BODY</label><textarea name="body" class="ftextarea" style="min-height:65px"></textarea></div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Alert</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-sec-title">ALL ALERTS (<?=count($alerts)?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>BUSINESS</th><th>TYPE</th><th>TITLE</th><th>STATUS</th><th>POSTED</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($alerts as $a): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($a['biz'])?></td>
|
||||
<td><span style="font-size:1rem"><?=alertIcon($a['type'])?></span> <?=e($a['type'])?></td>
|
||||
<td style="font-size:.85rem;max-width:220px"><?=e($a['title'])?></td>
|
||||
<td><?=$a['is_active']?'<span class="badge badge-green">Active</span>':'<span class="badge badge-gray">Inactive</span>'?></td>
|
||||
<td style="font-size:.8rem"><?=ago($a['created_at'])?></td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="toggle"><input type="hidden" name="id" value="<?=$a['id']?>"><button class="btn btn-xs btn-secondary"><?=$a['is_active']?'Disable':'Enable'?></button></form>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$a['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete alert?">🗑️</button></form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
$adminTitle = 'Attractions';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
$f = ['name'=>ps('name'),'category'=>ps('category'),'description'=>ps('description'),
|
||||
'address'=>ps('address'),'phone'=>ps('phone'),'website'=>ps('website'),
|
||||
'hours'=>ps('hours'),'is_active'=>(int)p('is_active',1),'sort_order'=>(int)p('sort_order',0)];
|
||||
if ($act === 'add') { if($f['name']) qrun("INSERT INTO attractions(name,category,description,address,phone,website,hours,is_active,sort_order)VALUES(?,?,?,?,?,?,?,?,?)",array_values($f)); flash('Attraction added!','success'); }
|
||||
if ($act === 'edit') { $id=(int)p('id'); if($f['name']) qrun("UPDATE attractions SET name=?,category=?,description=?,address=?,phone=?,website=?,hours=?,is_active=?,sort_order=? WHERE id=?",array_merge(array_values($f),[$id])); flash('Attraction updated!','success'); }
|
||||
if ($act === 'delete') { qrun("DELETE FROM attractions WHERE id=?",[(int)p('id')]); flash('Attraction deleted.','success'); }
|
||||
go('/admin/attractions.php');
|
||||
}
|
||||
|
||||
$editId = iget('edit');
|
||||
$editing = $editId ? qone("SELECT * FROM attractions WHERE id=?",[$editId]) : null;
|
||||
$attrs = qall("SELECT * FROM attractions ORDER BY sort_order,name");
|
||||
?>
|
||||
<?php adminTitle('Manage Attractions') ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title"><?=$editing?'✏️ EDIT: '.strtoupper(e($editing['name'])):'➕ ADD ATTRACTION'?></div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="<?=$editing?'edit':'add'?>">
|
||||
<?php if($editing): ?><input type="hidden" name="id" value="<?=$editing['id']?>"><?php endif; ?>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">NAME *</label><input type="text" name="name" class="finput" value="<?=e($editing['name']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">CATEGORY</label><input type="text" name="category" class="finput" value="<?=e($editing['category']??'Sightseeing')?>" placeholder="e.g. Natural Landmark, Historic Site"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><textarea name="description" class="ftextarea" style="min-height:80px"><?=e($editing['description']??'')?></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">ADDRESS</label><input type="text" name="address" class="finput" value="<?=e($editing['address']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">PHONE</label><input type="text" name="phone" class="finput" value="<?=e($editing['phone']??'')?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">WEBSITE</label><input type="text" name="website" class="finput" value="<?=e($editing['website']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">HOURS</label><input type="text" name="hours" class="finput" value="<?=e($editing['hours']??'')?>" placeholder="e.g. Open year-round, Dawn to Dusk"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">STATUS</label><select name="is_active" class="fselect"><option value="1"<?=($editing['is_active']??1)?' selected':''?>>Active</option><option value="0"<?=isset($editing)&&!$editing['is_active']?' selected':''?>>Hidden</option></select></div>
|
||||
<div class="fg"><label class="flabel">SORT ORDER</label><input type="number" name="sort_order" class="finput" value="<?=$editing['sort_order']??0?>"></div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary"><?=$editing?'Update':'Add Attraction'?></button>
|
||||
<?php if($editing): ?><a href="/admin/attractions.php" class="btn btn-secondary">Cancel</a><?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-sec-title">ALL ATTRACTIONS (<?=count($attrs)?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>NAME</th><th>CATEGORY</th><th>STATUS</th><th>ORDER</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($attrs as $a): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($a['name'])?></td>
|
||||
<td><span style="font-size:.78rem;color:var(--text-d)"><?=e($a['category'])?></span></td>
|
||||
<td><?=$a['is_active']?'<span class="badge badge-green">Active</span>':'<span class="badge badge-gray">Hidden</span>'?></td>
|
||||
<td><?=$a['sort_order']?></td>
|
||||
<td><div class="flex-row" style="gap:.3rem">
|
||||
<a href="/admin/attractions.php?edit=<?=$a['id']?>" class="btn btn-xs btn-outline">✏️ Edit</a>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$a['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete attraction?">🗑️</button></form>
|
||||
</div></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* Business DB Diagnostic — shows exact queries and results
|
||||
* Access: /admin/biz_debug.php
|
||||
* Remove this file from production after use.
|
||||
*/
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
requireAdmin();
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
$db = db();
|
||||
|
||||
echo "=== DISCOVER KEYSER WV — BUSINESS TABLE DIAGNOSTIC ===\n";
|
||||
echo "Generated: ".date('Y-m-d H:i:s')."\n";
|
||||
echo "DB File: ".DB_FILE."\n";
|
||||
echo "DB Writable: ".(is_writable(DB_FILE)?'YES':'NO')."\n\n";
|
||||
|
||||
// 1. Schema
|
||||
echo "--- SCHEMA (businesses table) ---\n";
|
||||
$schema = $db->query("SELECT sql FROM sqlite_master WHERE type='table' AND name='businesses'")->fetchColumn();
|
||||
echo $schema."\n\n";
|
||||
|
||||
// 2. Row count
|
||||
$count = $db->query("SELECT COUNT(*) FROM businesses")->fetchColumn();
|
||||
echo "--- ROW COUNT ---\n";
|
||||
echo "SELECT COUNT(*) FROM businesses;\n=> $count rows\n\n";
|
||||
|
||||
// 3. All businesses (id, name, slug, is_active)
|
||||
echo "--- ALL BUSINESSES ---\n";
|
||||
echo "SELECT id, name, slug, category_id, is_active, is_featured FROM businesses ORDER BY id;\n";
|
||||
$rows = $db->query("SELECT id, name, slug, category_id, is_active, is_featured FROM businesses ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $r) {
|
||||
echo sprintf(" [%3d] %-40s slug=%-35s cat=%s active=%d featured=%d\n",
|
||||
$r['id'], substr($r['name'],0,40), substr($r['slug'],0,35), $r['category_id'], $r['is_active'], $r['is_featured']);
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// 4. Test UPDATE with id=1
|
||||
$id = isset($_GET['test_id']) ? (int)$_GET['test_id'] : 1;
|
||||
$biz = $db->prepare("SELECT * FROM businesses WHERE id=?");
|
||||
$biz->execute([$id]);
|
||||
$row = $biz->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "--- FETCH SINGLE ROW (id=$id) ---\n";
|
||||
echo "SELECT * FROM businesses WHERE id=$id;\n";
|
||||
if ($row) {
|
||||
foreach ($row as $k => $v) {
|
||||
echo sprintf(" %-14s = %s\n", $k, strlen($v) > 80 ? substr($v,0,80).'...' : $v);
|
||||
}
|
||||
} else {
|
||||
echo " [NO ROW FOUND for id=$id]\n";
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// 5. Categories
|
||||
echo "--- CATEGORIES ---\n";
|
||||
echo "SELECT id, name, icon, sort_order FROM categories ORDER BY sort_order;\n";
|
||||
$cats = $db->query("SELECT id, name, icon, sort_order FROM categories ORDER BY sort_order")->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($cats as $c) {
|
||||
echo sprintf(" [%2d] %s %s (sort=%d)\n", $c['id'], $c['icon'], $c['name'], $c['sort_order']);
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// 6. Test UPDATE statement (dry run — shows exact SQL and params)
|
||||
if ($row) {
|
||||
echo "--- UPDATE STATEMENT THAT WOULD BE RUN ---\n";
|
||||
$sql = "UPDATE businesses SET name=?,slug=?,category_id=?,subcategory=?,description=?,address=?,phone=?,email=?,website=?,hours=?,lat=?,lng=?,is_active=?,is_featured=? WHERE id=?";
|
||||
echo $sql."\n";
|
||||
echo "Params (15 total):\n";
|
||||
$params = [
|
||||
$row['name'], $row['slug'], $row['category_id'], $row['subcategory'],
|
||||
$row['description'], $row['address'], $row['phone'], $row['email'],
|
||||
$row['website'], $row['hours'], $row['lat'], $row['lng'],
|
||||
$row['is_active'], $row['is_featured'], $id
|
||||
];
|
||||
foreach ($params as $i => $v) {
|
||||
echo sprintf(" [%2d] %s\n", $i+1, strlen((string)$v) > 80 ? substr($v,0,80).'...' : $v);
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// Actually execute the no-op UPDATE to verify it works
|
||||
echo "--- EXECUTING NO-OP UPDATE (same values back) ---\n";
|
||||
try {
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$affected = $stmt->rowCount();
|
||||
echo "SUCCESS: $affected row(s) affected\n\n";
|
||||
} catch (\Exception $ex) {
|
||||
echo "ERROR: ".$ex->getMessage()."\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Duplicate slug check
|
||||
echo "--- DUPLICATE SLUG CHECK ---\n";
|
||||
echo "SELECT slug, COUNT(*) c FROM businesses GROUP BY slug HAVING c > 1;\n";
|
||||
$dupes = $db->query("SELECT slug, COUNT(*) c FROM businesses GROUP BY slug HAVING c > 1")->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($dupes) {
|
||||
foreach ($dupes as $d) echo " DUPLICATE: {$d['slug']} ({$d['c']} times)\n";
|
||||
} else {
|
||||
echo " No duplicates found.\n";
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
// 8. NULL/empty slug check
|
||||
echo "--- NULL OR EMPTY SLUG CHECK ---\n";
|
||||
echo "SELECT id, name, slug FROM businesses WHERE slug IS NULL OR slug='';\n";
|
||||
$bad = $db->query("SELECT id, name, slug FROM businesses WHERE slug IS NULL OR slug=''")->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($bad) {
|
||||
foreach ($bad as $b) echo " BAD: [{$b['id']}] {$b['name']} slug=".var_export($b['slug'],true)."\n";
|
||||
} else {
|
||||
echo " All slugs OK.\n";
|
||||
}
|
||||
echo "\n";
|
||||
|
||||
echo "=== END DIAGNOSTIC ===\n";
|
||||
echo "Tip: Add ?test_id=N to test a specific business by ID.\n";
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* Dedicated Business Editor — standalone, no dependency on businesses.php
|
||||
* URL: /admin/biz_edit.php?id=N
|
||||
*/
|
||||
$adminTitle = 'Direct Business Editor';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
$id = iget('id');
|
||||
$msg = '';
|
||||
$err = '';
|
||||
|
||||
// ── Handle save ──────────────────────────────────────────
|
||||
if (isPost() && p('_act') === 'save') {
|
||||
csrfCheck();
|
||||
$id = (int)p('id');
|
||||
|
||||
if (!$id) { $err = 'No business ID provided.'; goto render; }
|
||||
|
||||
$name = ps('name');
|
||||
if (!$name) { $err = 'Business name is required.'; goto render; }
|
||||
|
||||
// Slug: use submitted or regenerate, ensure unique excluding self
|
||||
$baseSlug = ps('slug') ?: makeSlug($name);
|
||||
$slug = $baseSlug;
|
||||
$i = 0;
|
||||
while (true) {
|
||||
$candidate = $baseSlug . ($i ? "-$i" : '');
|
||||
$clash = qval("SELECT id FROM businesses WHERE slug=? AND id!=?", [$candidate, $id]);
|
||||
if (!$clash) { $slug = $candidate; break; }
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Hours
|
||||
$days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
$hrs = [];
|
||||
foreach ($days as $d) {
|
||||
$v = ps("h_$d");
|
||||
if ($v !== '') $hrs[$d] = $v;
|
||||
}
|
||||
|
||||
// Run individual named UPDATEs so a failure on one field is visible
|
||||
$updates = [
|
||||
'name' => ps('name'),
|
||||
'slug' => $slug,
|
||||
'category_id' => (int)p('category_id'),
|
||||
'subcategory' => ps('subcategory'),
|
||||
'description' => ps('description'),
|
||||
'address' => ps('address'),
|
||||
'phone' => ps('phone'),
|
||||
'email' => ps('email'),
|
||||
'website' => ps('website'),
|
||||
'hours' => json_encode($hrs),
|
||||
'lat' => (float)p('lat', 39.4364),
|
||||
'lng' => (float)p('lng', -78.9762),
|
||||
'is_active' => (int)p('is_active', 1),
|
||||
'is_featured' => (int)p('is_featured', 0),
|
||||
];
|
||||
|
||||
try {
|
||||
$setClauses = implode(', ', array_map(fn($k) => "$k=?", array_keys($updates)));
|
||||
$sql = "UPDATE businesses SET $setClauses WHERE id=?";
|
||||
$params = array_merge(array_values($updates), [$id]);
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$affected = $stmt->rowCount();
|
||||
$msg = "Saved successfully ($affected row updated). SQL: $sql";
|
||||
} catch (\Throwable $ex) {
|
||||
$err = 'DB ERROR: '.$ex->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
render:
|
||||
|
||||
// ── Load business ────────────────────────────────────────
|
||||
$biz = $id ? qone("SELECT * FROM businesses WHERE id=?", [$id]) : null;
|
||||
$cats = qall("SELECT * FROM categories ORDER BY sort_order");
|
||||
$days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
$h = $biz ? parseHours($biz['hours']) : [];
|
||||
|
||||
// All businesses for the picker
|
||||
$all = qall("SELECT id, name FROM businesses ORDER BY name");
|
||||
?>
|
||||
|
||||
<?php adminTitle('Direct Business Editor', 'Standalone editor — bypasses the main businesses page entirely.') ?>
|
||||
|
||||
<!-- Business picker -->
|
||||
<div class="ibox" style="margin-bottom:1.5rem;max-width:500px">
|
||||
<div class="ibox-title">SELECT BUSINESS TO EDIT</div>
|
||||
<form method="GET" action="/admin/biz_edit.php" style="display:flex;gap:.6rem">
|
||||
<select name="id" class="fselect" style="flex:1">
|
||||
<option value="">-- choose --</option>
|
||||
<?php foreach ($all as $b): ?>
|
||||
<option value="<?=$b['id']?>"<?= $id === $b['id'] ? ' selected' : '' ?>><?=e($b['name'])?> (ID <?=$b['id']?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Load</button>
|
||||
</form>
|
||||
<?php if ($all): ?>
|
||||
<p style="font-size:.78rem;color:var(--text-d);margin-top:.5rem">Or go directly: /admin/biz_edit.php?id=N</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Status messages -->
|
||||
<?php if ($msg): ?>
|
||||
<div style="background:rgba(42,122,71,.25);border:1px solid var(--green-l);color:#86e8a8;padding:.85rem 1.1rem;border-radius:6px;margin-bottom:1rem;font-size:.88rem;font-family:monospace;white-space:pre-wrap;word-break:break-all"><?=e($msg)?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($err): ?>
|
||||
<div style="background:rgba(143,45,45,.3);border:1px solid var(--red-l);color:#ff9a9a;padding:.85rem 1.1rem;border-radius:6px;margin-bottom:1rem;font-size:.88rem;font-family:monospace;white-space:pre-wrap;word-break:break-all"><?=e($err)?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($biz): ?>
|
||||
<!-- ── Edit form ─────────────────────────────────────────── -->
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">EDITING: <?=e($biz['name'])?> <span style="font-weight:400;color:var(--text-d)">(ID <?=$biz['id']?>)</span></div>
|
||||
|
||||
<form method="POST" action="/admin/biz_edit.php?id=<?=$biz['id']?>">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="save">
|
||||
<input type="hidden" name="id" value="<?=$biz['id']?>">
|
||||
|
||||
<!-- Name & Slug -->
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">NAME *</label>
|
||||
<input type="text" name="name" class="finput" value="<?=e($biz['name'])?>" required>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">SLUG</label>
|
||||
<input type="text" name="slug" class="finput" value="<?=e($biz['slug'])?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category & Subcategory -->
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">CATEGORY</label>
|
||||
<select name="category_id" class="fselect">
|
||||
<?php foreach ($cats as $c): ?>
|
||||
<option value="<?=$c['id']?>"<?= (int)$biz['category_id'] === (int)$c['id'] ? ' selected' : '' ?>><?=e($c['icon'].' '.$c['name'])?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">SUBCATEGORY</label>
|
||||
<input type="text" name="subcategory" class="finput" value="<?=e($biz['subcategory'])?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="fg">
|
||||
<label class="flabel">DESCRIPTION</label>
|
||||
<textarea name="description" class="ftextarea" style="min-height:100px"><?=e($biz['description'])?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Address & Phone -->
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">ADDRESS</label>
|
||||
<input type="text" name="address" class="finput" value="<?=e($biz['address'])?>">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">PHONE</label>
|
||||
<input type="text" name="phone" class="finput" value="<?=e($biz['phone'])?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email & Website -->
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">EMAIL</label>
|
||||
<input type="email" name="email" class="finput" value="<?=e($biz['email'])?>">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">WEBSITE (no https://)</label>
|
||||
<input type="text" name="website" class="finput" value="<?=e($biz['website'])?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lat & Lng -->
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">LATITUDE</label>
|
||||
<input type="number" name="lat" class="finput" value="<?=e($biz['lat'])?>" step="0.0001">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">LONGITUDE</label>
|
||||
<input type="number" name="lng" class="finput" value="<?=e($biz['lng'])?>" step="0.0001">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hours -->
|
||||
<div style="margin-bottom:1rem">
|
||||
<label class="flabel" style="margin-bottom:.6rem">HOURS OF OPERATION</label>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:.45rem">
|
||||
<?php foreach ($days as $d): ?>
|
||||
<div style="display:flex;gap:.4rem;align-items:center">
|
||||
<span style="font-family:'Oswald',sans-serif;font-size:.72rem;color:var(--gold);width:30px;flex-shrink:0"><?=$d?></span>
|
||||
<input type="text" name="h_<?=$d?>" class="finput" style="font-size:.8rem;padding:.38rem .6rem"
|
||||
value="<?=e($h[$d] ?? '')?>" placeholder="e.g. 9am-5pm">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status & Featured -->
|
||||
<div class="form-row" style="margin-bottom:.85rem">
|
||||
<div class="fg">
|
||||
<label class="flabel">STATUS</label>
|
||||
<select name="is_active" class="fselect">
|
||||
<option value="1"<?= $biz['is_active'] == 1 ? ' selected' : '' ?>>Active (visible)</option>
|
||||
<option value="0"<?= $biz['is_active'] == 0 ? ' selected' : '' ?>>Inactive (hidden)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">FEATURED</label>
|
||||
<select name="is_featured" class="fselect">
|
||||
<option value="0"<?= $biz['is_featured'] == 0 ? ' selected' : '' ?>>Not Featured</option>
|
||||
<option value="1"<?= $biz['is_featured'] == 1 ? ' selected' : '' ?>>Featured</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary">Save Business</button>
|
||||
<a href="/business.php?slug=<?=e($biz['slug'])?>" target="_blank" class="btn btn-outline btn-sm">View Listing</a>
|
||||
<a href="/admin/biz_edit.php" class="btn btn-secondary btn-sm">Pick Different</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Raw data panel -->
|
||||
<details style="margin-top:1rem">
|
||||
<summary style="cursor:pointer;font-size:.82rem;color:var(--text-d);font-family:'Oswald',sans-serif;letter-spacing:.08em">🔍 RAW DATABASE VALUES FOR THIS ROW</summary>
|
||||
<div style="background:var(--bg0);border:1px solid var(--bdr);border-radius:6px;padding:1rem;margin-top:.6rem;font-family:monospace;font-size:.78rem;color:var(--text-m)">
|
||||
<?php foreach ($biz as $col => $val): ?>
|
||||
<div style="display:flex;gap:.75rem;padding:.22rem 0;border-bottom:1px solid rgba(255,255,255,.04)">
|
||||
<span style="color:var(--gold);width:120px;flex-shrink:0"><?=e($col)?></span>
|
||||
<span style="word-break:break-all"><?=e(strlen((string)$val) > 200 ? substr($val,0,200).'...' : $val)?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<?php elseif ($id): ?>
|
||||
<div class="err-box">No business found with ID <?=(int)$id?>.</div>
|
||||
<?php else: ?>
|
||||
<div class="empty" style="padding:2rem">
|
||||
<div class="empty-ico">🏢</div>
|
||||
<h3>Select a business above to begin editing</h3>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
$adminTitle = 'Owner Edit Requests';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
// ── Field labels & apply logic ────────────────────────────
|
||||
$fieldLabels = [
|
||||
'description' => 'Description',
|
||||
'address' => 'Address',
|
||||
'phone' => 'Phone',
|
||||
'email' => 'Email',
|
||||
'hours' => 'Hours of Operation',
|
||||
'is_active' => 'Listing Status (Open/Closed)',
|
||||
];
|
||||
|
||||
// ── POST: approve / reject individual or all ─────────────
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
// Approve single field change
|
||||
if ($act === 'approve') {
|
||||
$reqId = (int)p('req_id');
|
||||
$req = qone("SELECT * FROM business_edit_requests WHERE id=?", [$reqId]);
|
||||
if ($req && $req['status'] === 'pending') {
|
||||
$field = $req['field'];
|
||||
$val = $req['new_value'];
|
||||
// Validate field is one we allow
|
||||
if (array_key_exists($field, $fieldLabels)) {
|
||||
if ($field === 'is_active') {
|
||||
qrun("UPDATE businesses SET is_active=? WHERE id=?", [(int)$val, $req['business_id']]);
|
||||
} elseif ($field === 'hours') {
|
||||
// Validate JSON before applying
|
||||
$decoded = json_decode($val, true);
|
||||
if (is_array($decoded)) {
|
||||
qrun("UPDATE businesses SET hours=? WHERE id=?", [$val, $req['business_id']]);
|
||||
}
|
||||
} else {
|
||||
qrun("UPDATE businesses SET $field=? WHERE id=?", [$val, $req['business_id']]);
|
||||
}
|
||||
qrun("UPDATE business_edit_requests SET status='approved', reviewed_at=datetime('now') WHERE id=?", [$reqId]);
|
||||
flash('Change approved and applied.', 'success');
|
||||
}
|
||||
}
|
||||
go('/admin/biz_edits.php'.(p('biz_id') ? '?biz_id='.p('biz_id') : ''));
|
||||
}
|
||||
|
||||
// Reject single field change
|
||||
if ($act === 'reject') {
|
||||
$reqId = (int)p('req_id');
|
||||
$notes = ps('admin_notes');
|
||||
qrun("UPDATE business_edit_requests SET status='rejected', admin_notes=?, reviewed_at=datetime('now') WHERE id=?",
|
||||
[$notes, $reqId]);
|
||||
flash('Change rejected.', 'info');
|
||||
go('/admin/biz_edits.php'.(p('biz_id') ? '?biz_id='.p('biz_id') : ''));
|
||||
}
|
||||
|
||||
// Approve ALL pending for a business
|
||||
if ($act === 'approve_all') {
|
||||
$bizId = (int)p('biz_id');
|
||||
$reqs = qall("SELECT * FROM business_edit_requests WHERE business_id=? AND status='pending'", [$bizId]);
|
||||
$count = 0;
|
||||
foreach ($reqs as $req) {
|
||||
$field = $req['field'];
|
||||
$val = $req['new_value'];
|
||||
if (!array_key_exists($field, $fieldLabels)) continue;
|
||||
if ($field === 'is_active') {
|
||||
qrun("UPDATE businesses SET is_active=? WHERE id=?", [(int)$val, $bizId]);
|
||||
} elseif ($field === 'hours') {
|
||||
$decoded = json_decode($val, true);
|
||||
if (is_array($decoded)) {
|
||||
qrun("UPDATE businesses SET hours=? WHERE id=?", [$val, $bizId]);
|
||||
}
|
||||
} else {
|
||||
qrun("UPDATE businesses SET $field=? WHERE id=?", [$val, $bizId]);
|
||||
}
|
||||
qrun("UPDATE business_edit_requests SET status='approved', reviewed_at=datetime('now') WHERE id=?", [$req['id']]);
|
||||
$count++;
|
||||
}
|
||||
flash("All $count pending change".($count!=1?'s':'')." approved and applied.", 'success');
|
||||
go('/admin/biz_edits.php?biz_id='.$bizId);
|
||||
}
|
||||
|
||||
// Reject ALL pending for a business
|
||||
if ($act === 'reject_all') {
|
||||
$bizId = (int)p('biz_id');
|
||||
$notes = ps('admin_notes');
|
||||
qrun("UPDATE business_edit_requests SET status='rejected', admin_notes=?, reviewed_at=datetime('now')
|
||||
WHERE business_id=? AND status='pending'",
|
||||
[$notes, $bizId]);
|
||||
flash('All pending changes rejected.', 'info');
|
||||
go('/admin/biz_edits.php?biz_id='.$bizId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filter by business ───────────────────────────────────
|
||||
$bizId = iget('biz_id');
|
||||
$filterStatus = gs('status') ?: 'pending';
|
||||
|
||||
// Grouped: businesses that have edit requests
|
||||
$bizWithEdits = qall("
|
||||
SELECT b.id, b.name, b.slug,
|
||||
COUNT(CASE WHEN ber.status='pending' THEN 1 END) AS pending_count,
|
||||
COUNT(CASE WHEN ber.status='approved' THEN 1 END) AS approved_count,
|
||||
COUNT(CASE WHEN ber.status='rejected' THEN 1 END) AS rejected_count,
|
||||
MAX(ber.created_at) AS latest_request
|
||||
FROM business_edit_requests ber
|
||||
JOIN businesses b ON b.id = ber.business_id
|
||||
GROUP BY b.id
|
||||
ORDER BY pending_count DESC, latest_request DESC
|
||||
");
|
||||
|
||||
// Requests for selected business
|
||||
$requests = [];
|
||||
$selectedBiz = null;
|
||||
if ($bizId) {
|
||||
$selectedBiz = qone("SELECT * FROM businesses WHERE id=?", [$bizId]);
|
||||
$whereStatus = $filterStatus === 'all' ? "" : "AND ber.status=?";
|
||||
$params = $filterStatus === 'all' ? [$bizId] : [$bizId, $filterStatus];
|
||||
$requests = qall("
|
||||
SELECT ber.*, u.username
|
||||
FROM business_edit_requests ber
|
||||
JOIN users u ON u.id = ber.user_id
|
||||
WHERE ber.business_id=? $whereStatus
|
||||
ORDER BY CASE ber.status WHEN 'pending' THEN 0 ELSE 1 END, ber.created_at DESC
|
||||
", $params);
|
||||
}
|
||||
?>
|
||||
<?php adminTitle('Owner Edit Requests', 'Review and approve or reject listing changes submitted by assigned business owners.') ?>
|
||||
|
||||
<div style="display:grid;grid-template-columns:280px 1fr;gap:1.5rem;align-items:start">
|
||||
|
||||
<!-- ── Left: business list ─────────────────────────────── -->
|
||||
<div>
|
||||
<div style="font-family:'Oswald',sans-serif;font-size:.72rem;letter-spacing:.18em;color:var(--gold);margin-bottom:.65rem">
|
||||
BUSINESSES WITH EDITS
|
||||
</div>
|
||||
<?php if ($bizWithEdits): ?>
|
||||
<?php foreach ($bizWithEdits as $bwe): ?>
|
||||
<a href="/admin/biz_edits.php?biz_id=<?=$bwe['id']?>"
|
||||
style="display:block;padding:.75rem 1rem;border-radius:var(--r);margin-bottom:.35rem;
|
||||
background:<?=$bizId===$bwe['id']?'var(--gold-dim)':'var(--bg3)'?>;
|
||||
border:1px solid <?=$bizId===$bwe['id']?'var(--gold-d)':'var(--bdr)'?>;
|
||||
text-decoration:none;transition:all .18s"
|
||||
class="<?=$bizId===$bwe['id']?'':'asl-hover'?>">
|
||||
<div style="font-weight:600;color:var(--text);font-size:.9rem;margin-bottom:.25rem"><?=e($bwe['name'])?></div>
|
||||
<div class="flex-row" style="gap:.4rem;flex-wrap:wrap">
|
||||
<?php if ($bwe['pending_count']): ?>
|
||||
<span class="badge badge-gold"><?=$bwe['pending_count']?> pending</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($bwe['approved_count']): ?>
|
||||
<span class="badge badge-green"><?=$bwe['approved_count']?> approved</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($bwe['rejected_count']): ?>
|
||||
<span class="badge badge-gray"><?=$bwe['rejected_count']?> rejected</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="font-size:.73rem;color:var(--text-d);margin-top:.25rem">
|
||||
Last: <?=ago($bwe['latest_request'] ?? '')?>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div style="color:var(--text-d);font-size:.88rem;padding:.75rem">No edit requests yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ── Right: requests for selected business ───────────── -->
|
||||
<div>
|
||||
<?php if ($selectedBiz): ?>
|
||||
|
||||
<!-- Business header -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem;margin-bottom:1.25rem">
|
||||
<div>
|
||||
<div style="font-family:'Oswald',sans-serif;font-size:1.1rem;color:var(--text)"><?=e($selectedBiz['name'])?></div>
|
||||
<div style="font-size:.82rem;color:var(--text-d)">
|
||||
<a href="/business.php?slug=<?=e($selectedBiz['slug'])?>" target="_blank" style="color:var(--gold)">View public listing ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status filter -->
|
||||
<div class="flex-row" style="gap:.4rem">
|
||||
<?php foreach(['pending'=>'Pending','approved'=>'Approved','rejected'=>'Rejected','all'=>'All'] as $k=>$lbl): ?>
|
||||
<a href="/admin/biz_edits.php?biz_id=<?=$bizId?>&status=<?=$k?>"
|
||||
class="btn btn-xs <?=$filterStatus===$k?'btn-primary':'btn-outline'?>"><?=$lbl?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$pendingReqs = array_filter($requests, fn($r) => $r['status'] === 'pending');
|
||||
?>
|
||||
|
||||
<!-- Approve all / Reject all bar -->
|
||||
<?php if (count($pendingReqs) > 1): ?>
|
||||
<div style="background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1rem 1.25rem;margin-bottom:1.25rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem">
|
||||
<span style="font-size:.88rem;color:var(--text-m)">
|
||||
<strong style="color:var(--gold)"><?=count($pendingReqs)?></strong> pending changes for this listing
|
||||
</span>
|
||||
<div class="flex-row" style="gap:.5rem">
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="approve_all">
|
||||
<input type="hidden" name="biz_id" value="<?=$bizId?>">
|
||||
<button class="btn btn-success btn-sm" data-confirm="Approve ALL <?=count($pendingReqs)?> pending changes for <?=e($selectedBiz['name'])?>?">
|
||||
✅ Approve All
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" id="rejectAllForm">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="reject_all">
|
||||
<input type="hidden" name="biz_id" value="<?=$bizId?>">
|
||||
<input type="text" name="admin_notes" class="finput" style="width:200px;font-size:.8rem;padding:.35rem .6rem" placeholder="Reason (optional)">
|
||||
<button class="btn btn-danger btn-sm" data-confirm="Reject ALL pending changes?">
|
||||
✕ Reject All
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Individual requests -->
|
||||
<?php if ($requests): ?>
|
||||
<?php foreach ($requests as $req): ?>
|
||||
<?php
|
||||
$isPending = $req['status'] === 'pending';
|
||||
$label = $fieldLabels[$req['field']] ?? ucfirst($req['field']);
|
||||
$isHours = $req['field'] === 'hours';
|
||||
$isStatus = $req['field'] === 'is_active';
|
||||
?>
|
||||
<div class="ibox" style="margin-bottom:1.1rem;<?= $isPending ? 'border-color:var(--gold-d)' : '' ?>">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:.75rem;flex-wrap:wrap">
|
||||
<div>
|
||||
<span style="font-family:'Oswald',sans-serif;font-size:.82rem;letter-spacing:.1em;color:var(--gold)"><?=e($label)?></span>
|
||||
<span style="margin-left:.6rem">
|
||||
<?php if ($isPending): ?>
|
||||
<span class="badge badge-gold">PENDING</span>
|
||||
<?php elseif ($req['status']==='approved'): ?>
|
||||
<span class="badge badge-green">APPROVED</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-gray">REJECTED</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span style="font-size:.75rem;color:var(--text-d);margin-left:.5rem">
|
||||
by <strong><?=e($req['username'] ?? '')?></strong> · <?=ago($req['created_at'] ?? '')?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Old vs New values -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin:1rem 0">
|
||||
<div>
|
||||
<div style="font-family:'Oswald',sans-serif;font-size:.65rem;letter-spacing:.16em;color:var(--text-d);margin-bottom:.4rem">CURRENT VALUE</div>
|
||||
<div style="background:var(--bg2);border:1px solid var(--bdr);border-radius:4px;padding:.65rem .85rem;font-size:.84rem;color:var(--text-m)">
|
||||
<?php if ($isHours): ?>
|
||||
<?php $oldH = parseHours($req['old_value']); ?>
|
||||
<?php if ($oldH): ?>
|
||||
<?php foreach ($oldH as $d => $t): ?>
|
||||
<div style="display:flex;gap:.5rem;font-size:.8rem"><span style="color:var(--text-d);min-width:32px"><?=e($d)?></span><span><?=e($t)?></span></div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?><em style="color:var(--text-d)">Not set</em><?php endif; ?>
|
||||
<?php elseif ($isStatus): ?>
|
||||
<?= $req['old_value'] === '1' ? '<span style="color:var(--green-l)">✅ Open / Active</span>' : '<span style="color:var(--text-d)">🔴 Closed / Inactive</span>' ?>
|
||||
<?php else: ?>
|
||||
<?= $req['old_value'] !== '' ? e($req['old_value']) : '<em style="color:var(--text-d)">Empty</em>' ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-family:'Oswald',sans-serif;font-size:.65rem;letter-spacing:.16em;color:var(--green-l);margin-bottom:.4rem">PROPOSED CHANGE</div>
|
||||
<div style="background:rgba(42,122,71,.1);border:1px solid rgba(58,165,94,.35);border-radius:4px;padding:.65rem .85rem;font-size:.84rem;color:var(--text)">
|
||||
<?php if ($isHours): ?>
|
||||
<?php $newH = parseHours($req['new_value']); ?>
|
||||
<?php if ($newH): ?>
|
||||
<?php foreach ($newH as $d => $t): ?>
|
||||
<div style="display:flex;gap:.5rem;font-size:.8rem"><span style="color:var(--gold);min-width:32px"><?=e($d)?></span><span><?=e($t)?></span></div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?><em style="color:var(--text-d)">Clear hours</em><?php endif; ?>
|
||||
<?php elseif ($isStatus): ?>
|
||||
<?= $req['new_value'] === '1' ? '<span style="color:var(--green-l)">✅ Open / Active</span>' : '<span style="color:var(--text-d)">🔴 Closed / Inactive</span>' ?>
|
||||
<?php else: ?>
|
||||
<?= $req['new_value'] !== '' ? e($req['new_value']) : '<em style="color:var(--text-d)">Empty</em>' ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($req['admin_notes'])): ?>
|
||||
<div style="font-size:.82rem;color:var(--text-d);margin-bottom:.75rem">
|
||||
<strong>Admin note:</strong> <?=e($req['admin_notes'])?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isPending): ?>
|
||||
<div class="flex-row" style="gap:.6rem;flex-wrap:wrap">
|
||||
<form method="POST" style="display:inline">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="approve">
|
||||
<input type="hidden" name="req_id" value="<?=$req['id']?>">
|
||||
<input type="hidden" name="biz_id" value="<?=$bizId?>">
|
||||
<button class="btn btn-success btn-sm">✅ Approve & Apply</button>
|
||||
</form>
|
||||
<form method="POST" style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="reject">
|
||||
<input type="hidden" name="req_id" value="<?=$req['id']?>">
|
||||
<input type="hidden" name="biz_id" value="<?=$bizId?>">
|
||||
<input type="text" name="admin_notes" class="finput"
|
||||
style="width:200px;font-size:.8rem;padding:.35rem .6rem"
|
||||
placeholder="Reason for rejection (optional)">
|
||||
<button class="btn btn-danger btn-sm">✕ Reject</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="empty" style="padding:2rem">
|
||||
<div class="empty-ico">📋</div>
|
||||
<h3>No <?= $filterStatus !== 'all' ? $filterStatus : '' ?> requests for this business</h3>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="empty" style="padding:3rem">
|
||||
<div class="empty-ico">📋</div>
|
||||
<h3>Select a business from the left to review its edit requests</h3>
|
||||
<?php if (empty($bizWithEdits)): ?>
|
||||
<p style="margin-top:.75rem;color:var(--text-d);font-size:.88rem">
|
||||
No edit requests have been submitted yet. Assigned owners can propose changes from their business listing page.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div><!-- /grid -->
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
$adminTitle = 'Businesses';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
// Handle POST actions
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'add' || $act === 'edit') {
|
||||
$id = (int)p('id');
|
||||
$name = ps('name');
|
||||
if (!$name) { flash('Business name is required.','error'); go('/admin/businesses.php'.($id?"?edit=$id":'')); }
|
||||
|
||||
$baseSlug = ps('slug') ?: makeSlug($name);
|
||||
|
||||
// Find a unique slug (excluding self on edit)
|
||||
$slug = $baseSlug; $i = 0;
|
||||
while (true) {
|
||||
$candidate = $baseSlug . ($i ? "-$i" : '');
|
||||
$clash = $act === 'edit'
|
||||
? qval("SELECT id FROM businesses WHERE slug=? AND id!=?", [$candidate, $id])
|
||||
: qval("SELECT id FROM businesses WHERE slug=?", [$candidate]);
|
||||
if (!$clash) { $slug = $candidate; break; }
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Build hours JSON
|
||||
$days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
$hrs = [];
|
||||
foreach ($days as $d) { $v = ps("hours_$d"); if ($v !== '') $hrs[$d] = $v; }
|
||||
|
||||
$fields = [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'category_id' => (int)p('category_id'),
|
||||
'subcategory' => ps('subcategory'),
|
||||
'description' => ps('description'),
|
||||
'address' => ps('address'),
|
||||
'phone' => ps('phone'),
|
||||
'email' => ps('email'),
|
||||
'website' => ps('website'),
|
||||
'hours' => json_encode($hrs),
|
||||
'lat' => (float)p('lat', 39.4364),
|
||||
'lng' => (float)p('lng', -78.9762),
|
||||
'is_active' => (int)p('is_active', 1),
|
||||
'is_featured' => (int)p('is_featured', 0),
|
||||
];
|
||||
|
||||
if ($act === 'add') {
|
||||
qrun("INSERT INTO businesses(name,slug,category_id,subcategory,description,address,phone,email,website,hours,lat,lng,is_active,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)", array_values($fields));
|
||||
flash('Business added!','success');
|
||||
go('/admin/businesses.php');
|
||||
} else {
|
||||
qrun("UPDATE businesses SET name=?,slug=?,category_id=?,subcategory=?,description=?,address=?,phone=?,email=?,website=?,hours=?,lat=?,lng=?,is_active=?,is_featured=? WHERE id=?", array_merge(array_values($fields), [$id]));
|
||||
flash('Business updated!','success');
|
||||
go('/admin/businesses.php?edit='.$id);
|
||||
}
|
||||
}
|
||||
|
||||
if ($act === 'delete') {
|
||||
$id = (int)p('id');
|
||||
qrun("DELETE FROM businesses WHERE id=?", [$id]);
|
||||
flash('Business deleted.','success');
|
||||
go('/admin/businesses.php');
|
||||
}
|
||||
|
||||
if ($act === 'toggle') {
|
||||
$id = (int)p('id');
|
||||
qrun("UPDATE businesses SET is_active = 1 - is_active WHERE id=?", [$id]);
|
||||
go('/admin/businesses.php');
|
||||
}
|
||||
}
|
||||
|
||||
$editId = iget('edit');
|
||||
$editing = $editId ? qone("SELECT * FROM businesses WHERE id=?", [$editId]) : null;
|
||||
$cats = qall("SELECT * FROM categories ORDER BY sort_order");
|
||||
$days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
$h = $editing ? parseHours($editing['hours']) : [];
|
||||
|
||||
$q = gs('q');
|
||||
$page = max(1, iget('page'));
|
||||
$perPage = 25;
|
||||
$where = $q ? "WHERE b.name LIKE ? OR b.address LIKE ? OR b.subcategory LIKE ?" : "";
|
||||
$params = $q ? ["%$q%", "%$q%", "%$q%"] : [];
|
||||
$total = (int)qval("SELECT COUNT(*) FROM businesses b $where", $params);
|
||||
$pages = max(1, (int)ceil($total / $perPage));
|
||||
$page = min($page, $pages);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$bizs = qall("SELECT b.*,c.name cat FROM businesses b LEFT JOIN categories c ON c.id=b.category_id $where ORDER BY b.name LIMIT $perPage OFFSET $offset", $params);
|
||||
?>
|
||||
<?php adminTitle($editing ? 'Edit Business: '.e($editing['name']) : 'Manage Businesses') ?>
|
||||
|
||||
<!-- Add/Edit Form -->
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title"><?= $editing ? '✏️ EDIT: '.strtoupper(e($editing['name'])) : '➕ ADD NEW BUSINESS' ?></div>
|
||||
<form method="POST" action="/admin/businesses.php<?= $editing ? '?edit='.$editing['id'] : '' ?>">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="<?= $editing ? 'edit' : 'add' ?>">
|
||||
<?php if ($editing): ?><input type="hidden" name="id" value="<?= $editing['id'] ?>"><?php endif; ?>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">BUSINESS NAME *</label>
|
||||
<input type="text" name="name" class="finput" value="<?=e($editing['name']??'')?>" required>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">SLUG (auto-generated if blank)</label>
|
||||
<input type="text" name="slug" class="finput" value="<?=e($editing['slug']??'')?>" placeholder="auto-generated from name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">CATEGORY</label>
|
||||
<select name="category_id" class="fselect">
|
||||
<?php foreach ($cats as $c): ?>
|
||||
<option value="<?=$c['id']?>"<?= (int)($editing['category_id']??0) === (int)$c['id'] ? ' selected' : '' ?>><?=e($c['icon'].' '.$c['name'])?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">SUBCATEGORY</label>
|
||||
<input type="text" name="subcategory" class="finput" value="<?=e($editing['subcategory']??'')?>" placeholder="e.g. Italian Restaurant">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">DESCRIPTION</label>
|
||||
<textarea name="description" class="ftextarea" style="min-height:90px"><?=e($editing['description']??'')?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">ADDRESS</label>
|
||||
<input type="text" name="address" class="finput" value="<?=e($editing['address']??'')?>">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">PHONE</label>
|
||||
<input type="text" name="phone" class="finput" value="<?=e($editing['phone']??'')?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">EMAIL</label>
|
||||
<input type="email" name="email" class="finput" value="<?=e($editing['email']??'')?>">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">WEBSITE (no https://)</label>
|
||||
<input type="text" name="website" class="finput" value="<?=e($editing['website']??'')?>" placeholder="example.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">LATITUDE</label>
|
||||
<input type="number" name="lat" class="finput" value="<?=e($editing['lat']??39.4364)?>" step="0.0001">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">LONGITUDE</label>
|
||||
<input type="number" name="lng" class="finput" value="<?=e($editing['lng']??-78.9762)?>" step="0.0001">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:1rem">
|
||||
<label class="flabel" style="margin-bottom:.6rem">HOURS OF OPERATION</label>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.5rem">
|
||||
<?php foreach ($days as $d): ?>
|
||||
<div style="display:flex;gap:.4rem;align-items:center">
|
||||
<span style="font-family:'Oswald',sans-serif;font-size:.72rem;color:var(--gold);width:28px;flex-shrink:0"><?=$d?></span>
|
||||
<input type="text" name="hours_<?=$d?>" class="finput" style="font-size:.8rem;padding:.4rem .6rem"
|
||||
value="<?=e($h[$d]??'')?>" placeholder="e.g. 9am-5pm">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-bottom:.75rem">
|
||||
<div class="fg">
|
||||
<label class="flabel">STATUS</label>
|
||||
<select name="is_active" class="fselect">
|
||||
<option value="1"<?= ($editing['is_active'] ?? 1) == 1 ? ' selected' : '' ?>>Active</option>
|
||||
<option value="0"<?= ($editing['is_active'] ?? 1) == 0 ? ' selected' : '' ?>>Inactive (hidden)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">FEATURED</label>
|
||||
<select name="is_featured" class="fselect">
|
||||
<option value="0"<?= ($editing['is_featured'] ?? 0) == 0 ? ' selected' : '' ?>>Not Featured</option>
|
||||
<option value="1"<?= ($editing['is_featured'] ?? 0) == 1 ? ' selected' : '' ?>>Featured</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary"><?= $editing ? 'Update Business' : 'Add Business' ?></button>
|
||||
<?php if ($editing): ?>
|
||||
<a href="/admin/businesses.php" class="btn btn-secondary">Cancel / Add New</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Businesses list -->
|
||||
<div class="admin-sec-title">ALL BUSINESSES (<?=$total?>)</div>
|
||||
<form method="GET" action="/admin/businesses.php" style="display:flex;gap:.5rem;margin-bottom:1rem">
|
||||
<input type="text" name="q" class="finput" style="max-width:300px" value="<?=e($q)?>" placeholder="Search businesses...">
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Search</button>
|
||||
<?php if ($q): ?><a href="/admin/businesses.php" class="btn btn-outline btn-sm">Clear</a><?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead>
|
||||
<tr><th>NAME</th><th>CATEGORY</th><th>PHONE</th><th>STATUS</th><th>FEATURED</th><th>ACTIONS</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($bizs as $b): ?>
|
||||
<tr<?= ($editing && $editing['id'] == $b['id']) ? ' style="background:rgba(201,168,76,.06)"' : '' ?>>
|
||||
<td><a href="/business.php?slug=<?=e($b['slug'])?>" class="td-name" target="_blank"><?=e($b['name'])?></a></td>
|
||||
<td><span style="font-size:.78rem;color:var(--text-d)"><?=e($b['cat']??'--')?></span></td>
|
||||
<td style="font-size:.8rem"><?=e($b['phone'])?></td>
|
||||
<td><?= $b['is_active'] ? '<span class="badge badge-green">Active</span>' : '<span class="badge badge-gray">Inactive</span>' ?></td>
|
||||
<td><?= $b['is_featured'] ? '<span class="badge badge-gold">Featured</span>' : '--' ?></td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<a href="/admin/businesses.php?edit=<?=$b['id']?>" class="btn btn-xs <?= ($editing && $editing['id']==$b['id']) ? 'btn-primary' : 'btn-outline' ?>">Edit</a>
|
||||
<form method="POST" action="/admin/businesses.php" style="display:inline">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="toggle">
|
||||
<input type="hidden" name="id" value="<?=$b['id']?>">
|
||||
<button class="btn btn-xs btn-secondary"><?= $b['is_active'] ? 'Deactivate' : 'Activate' ?></button>
|
||||
</form>
|
||||
<form method="POST" action="/admin/businesses.php" style="display:inline">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="delete">
|
||||
<input type="hidden" name="id" value="<?=$b['id']?>">
|
||||
<button class="btn btn-xs btn-danger" data-confirm="Permanently delete '<?=e($b['name'])?>' and all its data?">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($pages > 1): ?>
|
||||
<div style="display:flex;gap:.5rem;margin-top:1rem;flex-wrap:wrap">
|
||||
<?php for ($i = 1; $i <= $pages; $i++): ?>
|
||||
<a href="?<?=http_build_query(array_filter(['q'=>$q,'page'=>$i]))?>" class="btn btn-sm<?= $i===$page ? ' btn-primary' : ' btn-outline'?>"><?=$i?></a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
$adminTitle = 'Categories';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
if ($act === 'add') {
|
||||
$name = ps('name'); $icon = ps('icon') ?: '🏢'; $sort = (int)p('sort_order',0);
|
||||
if ($name) { qrun("INSERT OR IGNORE INTO categories(name,icon,sort_order)VALUES(?,?,?)",[$name,$icon,$sort]); flash('Category added!','success'); }
|
||||
}
|
||||
if ($act === 'edit') {
|
||||
qrun("UPDATE categories SET name=?,icon=?,sort_order=? WHERE id=?",[ps('name'),ps('icon'),(int)p('sort_order'),(int)p('id')]);
|
||||
flash('Category updated!','success');
|
||||
}
|
||||
if ($act === 'delete') {
|
||||
$id = (int)p('id');
|
||||
$cnt = (int)qval("SELECT COUNT(*) FROM businesses WHERE category_id=?",[$id]);
|
||||
if ($cnt > 0) { flash("Cannot delete — $cnt businesses use this category.",'error'); }
|
||||
else { qrun("DELETE FROM categories WHERE id=?",[$id]); flash('Category deleted.','success'); }
|
||||
}
|
||||
go('/admin/categories.php');
|
||||
}
|
||||
|
||||
$editId = iget('edit');
|
||||
$editing = $editId ? qone("SELECT * FROM categories WHERE id=?",[$editId]) : null;
|
||||
$cats = qall("SELECT c.*,(SELECT COUNT(*) FROM businesses WHERE category_id=c.id) biz_count FROM categories c ORDER BY c.sort_order,c.name");
|
||||
?>
|
||||
<?php adminTitle('Manage Business Categories') ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem;max-width:600px">
|
||||
<div class="ibox-title"><?=$editing?'✏️ EDIT CATEGORY':'➕ ADD CATEGORY'?></div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="<?=$editing?'edit':'add'?>">
|
||||
<?php if($editing): ?><input type="hidden" name="id" value="<?=$editing['id']?>"><?php endif; ?>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">NAME *</label><input type="text" name="name" class="finput" value="<?=e($editing['name']??'')?>" required placeholder="e.g. Dining & Food"></div>
|
||||
<div class="fg"><label class="flabel">EMOJI ICON</label><input type="text" name="icon" class="finput" value="<?=e($editing['icon']??'🏢')?>" placeholder="🏢" style="font-size:1.2rem"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">SORT ORDER</label><input type="number" name="sort_order" class="finput" value="<?=$editing['sort_order']??0?>" style="max-width:120px"></div>
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary btn-sm"><?=$editing?'Update':'Add Category'?></button>
|
||||
<?php if($editing): ?><a href="/admin/categories.php" class="btn btn-secondary btn-sm">Cancel</a><?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-sec-title">ALL CATEGORIES (<?=count($cats)?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>ICON</th><th>NAME</th><th>BUSINESSES</th><th>SORT</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($cats as $c): ?>
|
||||
<tr>
|
||||
<td style="font-size:1.4rem;text-align:center"><?=$c['icon']?></td>
|
||||
<td class="td-name"><?=e($c['name'])?></td>
|
||||
<td><span class="badge badge-blue"><?=$c['biz_count']?></span></td>
|
||||
<td><?=$c['sort_order']?></td>
|
||||
<td><div class="flex-row" style="gap:.3rem">
|
||||
<a href="/admin/categories.php?edit=<?=$c['id']?>" class="btn btn-xs btn-outline">✏️ Edit</a>
|
||||
<?php if(!$c['biz_count']): ?>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$c['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete category?">🗑️</button></form>
|
||||
<?php else: ?><span style="font-size:.75rem;color:var(--text-d)">In use</span><?php endif; ?>
|
||||
</div></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
$adminTitle = 'Events';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'approve') { qrun("UPDATE events SET status='approved' WHERE id=?",[(int)p('id')]); flash('Event approved!','success'); go('/admin/events.php'); }
|
||||
if ($act === 'reject') { qrun("UPDATE events SET status='rejected' WHERE id=?",[(int)p('id')]); flash('Event rejected.','success'); go('/admin/events.php'); }
|
||||
if ($act === 'delete') { qrun("DELETE FROM events WHERE id=?",[(int)p('id')]); flash('Event deleted.','success'); go('/admin/events.php'); }
|
||||
if ($act === 'feature') { qrun("UPDATE events SET is_featured=1-is_featured WHERE id=?",[(int)p('id')]); go('/admin/events.php'); }
|
||||
|
||||
if ($act === 'add' || $act === 'edit') {
|
||||
$f=['title'=>ps('title'),'description'=>ps('description'),'venue'=>ps('venue'),'address'=>ps('address'),
|
||||
'event_date'=>ps('event_date'),'event_time'=>ps('event_time'),'end_date'=>ps('end_date')?:null,
|
||||
'end_time'=>ps('end_time'),'cost'=>ps('cost'),'website'=>ps('website'),'contact_name'=>ps('contact_name'),
|
||||
'contact_email'=>ps('contact_email'),'contact_phone'=>ps('contact_phone'),
|
||||
'status'=>p('status','approved'),'is_featured'=>(int)p('is_featured',0)];
|
||||
if (!$f['title'] || !$f['event_date']) { flash('Title and date required.','error'); go('/admin/events.php'); }
|
||||
if ($act === 'add') {
|
||||
qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,status,is_featured,submitted_by)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",array_merge(array_values($f),[uid()]));
|
||||
flash('Event added!','success');
|
||||
} else {
|
||||
$id = (int)p('id');
|
||||
qrun("UPDATE events SET title=?,description=?,venue=?,address=?,event_date=?,event_time=?,end_date=?,end_time=?,cost=?,website=?,contact_name=?,contact_email=?,contact_phone=?,status=?,is_featured=? WHERE id=?",array_merge(array_values($f),[$id]));
|
||||
flash('Event updated!','success');
|
||||
}
|
||||
go('/admin/events.php');
|
||||
}
|
||||
}
|
||||
|
||||
$filter = gs('filter') ?: 'all';
|
||||
$editId = iget('edit');
|
||||
$editing = $editId ? qone("SELECT * FROM events WHERE id=?",[$editId]) : null;
|
||||
$where = match($filter) { 'pending'=>"WHERE status='pending'",'approved'=>"WHERE status='approved'", default=>"" };
|
||||
$events = qall("SELECT e.*,u.username FROM events e LEFT JOIN users u ON u.id=e.submitted_by $where ORDER BY CASE status WHEN 'pending' THEN 0 ELSE 1 END,event_date DESC");
|
||||
?>
|
||||
<?php adminTitle('Manage Events') ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title"><?=$editing ? '✏️ EDIT EVENT: '.strtoupper(e($editing['title'])) : '➕ ADD EVENT'?></div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="<?=$editing?'edit':'add'?>">
|
||||
<?php if($editing): ?><input type="hidden" name="id" value="<?=$editing['id']?>"><?php endif; ?>
|
||||
<div class="fg"><label class="flabel">TITLE *</label><input type="text" name="title" class="finput" value="<?=e($editing['title']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><textarea name="description" class="ftextarea" style="min-height:80px"><?=e($editing['description']??'')?></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">VENUE</label><input type="text" name="venue" class="finput" value="<?=e($editing['venue']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">ADDRESS</label><input type="text" name="address" class="finput" value="<?=e($editing['address']??'')?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">START DATE *</label><input type="date" name="event_date" class="finput" value="<?=e($editing['event_date']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">START TIME</label><input type="text" name="event_time" class="finput" value="<?=e($editing['event_time']??'')?>" placeholder="e.g. 7:00 PM"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">END DATE</label><input type="date" name="end_date" class="finput" value="<?=e($editing['end_date']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">END TIME</label><input type="text" name="end_time" class="finput" value="<?=e($editing['end_time']??'')?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">COST</label><input type="text" name="cost" class="finput" value="<?=e($editing['cost']??'Free')?>"></div>
|
||||
<div class="fg"><label class="flabel">WEBSITE</label><input type="text" name="website" class="finput" value="<?=e($editing['website']??'')?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">CONTACT NAME</label><input type="text" name="contact_name" class="finput" value="<?=e($editing['contact_name']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">CONTACT PHONE</label><input type="text" name="contact_phone" class="finput" value="<?=e($editing['contact_phone']??'')?>"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">CONTACT EMAIL</label><input type="email" name="contact_email" class="finput" value="<?=e($editing['contact_email']??'')?>"></div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">STATUS</label><select name="status" class="fselect"><option value="approved"<?=($editing['status']??'approved')==='approved'?' selected':''?>>Approved (public)</option><option value="pending"<?=($editing['status']??'')==='pending'?' selected':''?>>Pending review</option><option value="rejected"<?=($editing['status']??'')==='rejected'?' selected':''?>>Rejected</option></select></div>
|
||||
<div class="fg"><label class="flabel">FEATURED</label><select name="is_featured" class="fselect"><option value="0"<?=!($editing['is_featured']??0)?' selected':''?>>Normal</option><option value="1"<?=($editing['is_featured']??0)?' selected':''?>>★ Featured</option></select></div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary"><?=$editing?'Update Event':'Add Event'?></button>
|
||||
<?php if($editing): ?><a href="/admin/events.php" class="btn btn-secondary">Cancel</a><?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.5rem;margin-bottom:.75rem">
|
||||
<div class="admin-sec-title" style="margin-bottom:0">ALL EVENTS (<?=count($events)?>)</div>
|
||||
<div class="flex-row" style="gap:.4rem">
|
||||
<?php foreach(['all'=>'All','pending'=>'Pending','approved'=>'Approved'] as $k=>$lbl): ?>
|
||||
<a href="/admin/events.php?filter=<?=$k?>" class="btn btn-xs<?=$filter===$k?' btn-primary':' btn-outline'?>"><?=$lbl?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>TITLE</th><th>DATE</th><th>SUBMITTED BY</th><th>STATUS</th><th>FEATURED</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($events as $ev): ?>
|
||||
<tr>
|
||||
<td class="td-name" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?=e($ev['title'])?></td>
|
||||
<td style="font-size:.8rem"><?=fdate($ev['event_date'])?></td>
|
||||
<td style="font-size:.8rem"><?=e($ev['username']??'Admin')?></td>
|
||||
<td><?php $sc=['approved'=>'badge-green','pending'=>'badge-gold','rejected'=>'badge-red']; ?><span class="badge <?=$sc[$ev['status']]??"badge-gray"?>"><?=strtoupper($ev['status'])?></span></td>
|
||||
<td><?=$ev['is_featured']?'<span class="badge badge-gold">★</span>':'—'?></td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<a href="/admin/events.php?edit=<?=$ev['id']?>" class="btn btn-xs btn-outline">✏️</a>
|
||||
<?php if($ev['status']==='pending'): ?><form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="approve"><input type="hidden" name="id" value="<?=$ev['id']?>"><button class="btn btn-xs btn-success">Approve</button></form><form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="reject"><input type="hidden" name="id" value="<?=$ev['id']?>"><button class="btn btn-xs btn-secondary">Reject</button></form><?php endif; ?>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="feature"><input type="hidden" name="id" value="<?=$ev['id']?>"><button class="btn btn-xs btn-secondary"><?=$ev['is_featured']?'Unfeature':'Feature'?></button></form>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$ev['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete event?">🗑️</button></form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
$adminTitle = 'Dashboard';
|
||||
include __DIR__.'/admin_header.php';
|
||||
$stats = [
|
||||
'Businesses' => qval("SELECT COUNT(*) FROM businesses WHERE is_active=1"),
|
||||
'Users' => qval("SELECT COUNT(*) FROM users"),
|
||||
'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 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"),
|
||||
];
|
||||
$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");
|
||||
$pendingEdits = qall("
|
||||
SELECT ber.*, b.name biz_name, b.slug biz_slug, b.id biz_id, u.username
|
||||
FROM business_edit_requests ber
|
||||
JOIN businesses b ON b.id = ber.business_id
|
||||
JOIN users u ON u.id = ber.user_id
|
||||
WHERE ber.status = 'pending'
|
||||
ORDER BY ber.created_at DESC
|
||||
LIMIT 10
|
||||
");
|
||||
?>
|
||||
<?php adminTitle('Site Overview','Welcome to the Discover Keyser WV administration panel.') ?>
|
||||
|
||||
<div class="stat-cards">
|
||||
<?php foreach($stats as $lbl=>$val): ?>
|
||||
<div class="stat-card">
|
||||
<div class="sc-n" style="<?=$lbl==='Pending Edits'&&$val>0?'color:var(--gold)':($lbl==='Open Reports'&&$val>0?'color:var(--red-l)':'')?>">
|
||||
<?=$val?>
|
||||
</div>
|
||||
<div class="sc-l"><?=e($lbl)?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Pending owner edits — top priority -->
|
||||
<?php if ($pendingEdits): ?>
|
||||
<div style="margin-bottom:2rem">
|
||||
<div class="admin-sec-title" style="color:var(--gold)">
|
||||
✏️ PENDING OWNER EDIT REQUESTS (<?=count($pendingEdits)?>)
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>BUSINESS</th><th>FIELD</th><th>SUBMITTED BY</th><th>PROPOSED VALUE</th><th>SUBMITTED</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$fieldLabels = ['description'=>'Description','address'=>'Address','phone'=>'Phone',
|
||||
'email'=>'Email','hours'=>'Hours','is_active'=>'Status'];
|
||||
foreach ($pendingEdits as $pe):
|
||||
$preview = $pe['field'] === 'hours'
|
||||
? '(hours update)'
|
||||
: ($pe['field'] === 'is_active'
|
||||
? ($pe['new_value']==='1' ? 'Open/Active' : 'Closed/Inactive')
|
||||
: substr($pe['new_value'], 0, 60).(strlen($pe['new_value'])>60?'…':''));
|
||||
?>
|
||||
<tr>
|
||||
<td><a href="/business.php?slug=<?=e($pe['biz_slug'])?>" target="_blank" class="td-name"><?=e($pe['biz_name'])?></a></td>
|
||||
<td><span class="badge badge-gold"><?=e($fieldLabels[$pe['field']] ?? $pe['field'])?></span></td>
|
||||
<td style="font-size:.82rem"><?=e($pe['username'] ?? '')?></td>
|
||||
<td style="font-size:.82rem;max-width:220px;color:var(--text-m)"><?=e($preview)?></td>
|
||||
<td style="font-size:.78rem;color:var(--text-d)"><?=ago($pe['created_at'] ?? '')?></td>
|
||||
<td><a href="/admin/biz_edits.php?biz_id=<?=$pe['biz_id']?>" class="btn btn-xs btn-primary">Review</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/admin/biz_edits.php" class="btn btn-outline btn-sm" style="margin-top:.75rem">View All Edit Requests</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="g2" style="gap:1.5rem;margin-bottom:2rem">
|
||||
<?php if($pendingEvts): ?>
|
||||
<div>
|
||||
<div class="admin-sec-title">⏳ PENDING EVENTS (<?=count($pendingEvts)?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>EVENT</th><th>SUBMITTED BY</th><th>DATE</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($pendingEvts as $ev): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($ev['title'])?></td>
|
||||
<td><?=e($ev['username']??'—')?></td>
|
||||
<td><?=fdate($ev['event_date']??'')?></td>
|
||||
<td><a href="/admin/events.php?edit=<?=$ev['id']?>" class="btn btn-xs btn-primary">Review</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($openReports): ?>
|
||||
<div>
|
||||
<div class="admin-sec-title">🚩 OPEN REPORTS (<?=count($openReports)?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>BUSINESS</th><th>TYPE</th><th>FROM</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($openReports as $r): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($r['biz'])?></td>
|
||||
<td><span class="badge badge-gold"><?=e($r['type'])?></span></td>
|
||||
<td><?=e($r['reporter'])?></td>
|
||||
<td><a href="/admin/reports.php" class="btn btn-xs btn-outline">View</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="g2">
|
||||
<div>
|
||||
<div class="admin-sec-title">🏢 RECENT BUSINESSES</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>NAME</th><th>STATUS</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($recentBiz as $b): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($b['name'])?></td>
|
||||
<td><?=$b['is_active']?'<span class="badge badge-green">Active</span>':'<span class="badge badge-gray">Inactive</span>'?></td>
|
||||
<td><a href="/admin/businesses.php?edit=<?=$b['id']?>" class="btn btn-xs btn-outline">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/admin/businesses.php" class="btn btn-outline btn-sm" style="margin-top:.75rem">Manage All Businesses</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="admin-sec-title">👥 RECENT USERS</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>USERNAME</th><th>JOINED</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($recentUsers as $u): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($u['username'])?><?=$u['is_admin']?' <span class="badge badge-gold">ADMIN</span>':''?></td>
|
||||
<td><?=fdate($u['created_at']??'')?></td>
|
||||
<td><a href="/admin/users.php?edit=<?=$u['id']?>" class="btn btn-xs btn-outline">Edit</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/admin/users.php" class="btn btn-outline btn-sm" style="margin-top:.75rem">Manage All Users</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
$adminTitle = 'Menus';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
$bizId = iget('biz_id');
|
||||
$biz = $bizId ? qone("SELECT * FROM businesses WHERE id=?",[$bizId]) : null;
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'add_section') {
|
||||
$bid = (int)p('business_id'); $name = ps('sec_name');
|
||||
if ($bid && $name) {
|
||||
$ord = (int)qval("SELECT COALESCE(MAX(sort_order),0)+1 FROM menu_sections WHERE business_id=?",[$bid]);
|
||||
qrun("INSERT INTO menu_sections(business_id,name,description,sort_order)VALUES(?,?,?,?)",[$bid,$name,ps('sec_desc'),$ord]);
|
||||
flash('Section added!','success');
|
||||
}
|
||||
go("/admin/menus.php?biz_id=$bid");
|
||||
}
|
||||
if ($act === 'edit_section') {
|
||||
$sid = (int)p('sid');
|
||||
qrun("UPDATE menu_sections SET name=?,description=?,sort_order=? WHERE id=?",[ps('sec_name'),ps('sec_desc'),(int)p('sec_order'),$sid]);
|
||||
flash('Section updated!','success'); go("/admin/menus.php?biz_id=$bizId");
|
||||
}
|
||||
if ($act === 'del_section') {
|
||||
qrun("DELETE FROM menu_sections WHERE id=?",[(int)p('sid')]);
|
||||
flash('Section deleted.','success'); go("/admin/menus.php?biz_id=$bizId");
|
||||
}
|
||||
if ($act === 'add_item') {
|
||||
$sid = (int)p('section_id');
|
||||
$name = ps('item_name');
|
||||
if ($sid && $name) {
|
||||
$ord = (int)qval("SELECT COALESCE(MAX(sort_order),0)+1 FROM menu_items WHERE section_id=?",[$sid]);
|
||||
qrun("INSERT INTO menu_items(section_id,name,description,price,is_available,sort_order)VALUES(?,?,?,?,?,?)",[$sid,$name,ps('item_desc'),(float)p('item_price',0),(int)p('is_available',1),$ord]);
|
||||
flash('Item added!','success');
|
||||
}
|
||||
go("/admin/menus.php?biz_id=$bizId");
|
||||
}
|
||||
if ($act === 'edit_item') {
|
||||
$iid = (int)p('iid');
|
||||
qrun("UPDATE menu_items SET name=?,description=?,price=?,is_available=?,sort_order=? WHERE id=?",[ps('item_name'),ps('item_desc'),(float)p('item_price',0),(int)p('is_available',1),(int)p('item_order'),$iid]);
|
||||
flash('Item updated!','success'); go("/admin/menus.php?biz_id=$bizId");
|
||||
}
|
||||
if ($act === 'del_item') {
|
||||
qrun("DELETE FROM menu_items WHERE id=?",[(int)p('iid')]);
|
||||
flash('Item removed.','success'); go("/admin/menus.php?biz_id=$bizId");
|
||||
}
|
||||
}
|
||||
|
||||
$bizList = qall("SELECT id,name FROM businesses WHERE is_active=1 ORDER BY name");
|
||||
|
||||
if ($biz) {
|
||||
$sections = qall("SELECT * FROM menu_sections WHERE business_id=? ORDER BY sort_order",[$bizId]);
|
||||
$items = [];
|
||||
foreach ($sections as $s) $items[$s['id']] = qall("SELECT * FROM menu_items WHERE section_id=? ORDER BY sort_order",[$s['id']]);
|
||||
}
|
||||
?>
|
||||
<?php adminTitle('Manage Menus','Edit menus, sections, and items for any business.') ?>
|
||||
|
||||
<!-- Business selector -->
|
||||
<div class="ibox" style="max-width:500px;margin-bottom:2rem">
|
||||
<div class="ibox-title">SELECT BUSINESS TO MANAGE MENU</div>
|
||||
<form method="GET" style="display:flex;gap:.65rem">
|
||||
<select name="biz_id" class="fselect" style="flex:1">
|
||||
<option value="">— Choose a business —</option>
|
||||
<?php foreach($bizList as $b): ?><option value="<?=$b['id']?>"<?=$bizId===$b['id']?' selected':''?>><?=e($b['name'])?></option><?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Load Menu</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if($biz): ?>
|
||||
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem">
|
||||
<h2 style="font-size:1.3rem;color:var(--gold);font-family:'Oswald',sans-serif;letter-spacing:.1em"><?=e($biz['name'])?> — Menu</h2>
|
||||
<a href="/business.php?slug=<?=e($biz['slug'])?>" target="_blank" class="btn btn-outline btn-xs">View Listing ↗</a>
|
||||
</div>
|
||||
|
||||
<!-- Add section -->
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:1.5rem">
|
||||
<div class="ibox-title">➕ ADD MENU SECTION</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add_section"><input type="hidden" name="business_id" value="<?=$bizId?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">SECTION NAME *</label><input type="text" name="sec_name" class="finput" required placeholder="e.g. Appetizers, Pasta, Desserts"></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="sec_desc" class="finput" placeholder="Optional description"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add Section</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php foreach($sections as $sec): ?>
|
||||
<div class="ibox" style="margin-bottom:1.5rem">
|
||||
<!-- Section header -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.9rem;padding-bottom:.6rem;border-bottom:1px solid var(--bdr)">
|
||||
<div>
|
||||
<span style="font-family:'Oswald',sans-serif;color:var(--gold);letter-spacing:.1em"><?=e($sec['name'])?></span>
|
||||
<?php if($sec['description']): ?><span style="font-size:.78rem;color:var(--text-d);margin-left:.5rem"><?=e($sec['description'])?></span><?php endif; ?>
|
||||
</div>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<button onclick="document.getElementById('esf<?=$sec['id']?>').classList.toggle('hidden')" class="btn btn-xs btn-secondary">✏️ Edit Section</button>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="del_section"><input type="hidden" name="sid" value="<?=$sec['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete section and ALL its items?">🗑️ Delete</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Edit section -->
|
||||
<div id="esf<?=$sec['id']?>" class="hidden" style="background:var(--bg4);border:1px solid var(--bdr);border-radius:4px;padding:.85rem;margin-bottom:.85rem">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="edit_section"><input type="hidden" name="sid" value="<?=$sec['id']?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">NAME</label><input type="text" name="sec_name" class="finput" value="<?=e($sec['name'])?>" required></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="sec_desc" class="finput" value="<?=e($sec['description'])?>"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">SORT ORDER</label><input type="number" name="sec_order" class="finput" style="max-width:100px" value="<?=$sec['sort_order']?>"></div>
|
||||
<button type="submit" class="btn btn-success btn-sm">Save Section</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
<?php if($items[$sec['id']]): ?>
|
||||
<div class="tbl-wrap" style="margin-bottom:.85rem">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>ITEM NAME</th><th>DESCRIPTION</th><th>PRICE</th><th>AVAIL.</th><th>ORDER</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($items[$sec['id']] as $item): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($item['name'])?></td>
|
||||
<td style="font-size:.8rem;max-width:180px"><?=e(substr($item['description'],0,80))?></td>
|
||||
<td style="color:var(--gold);font-family:'Oswald',sans-serif"><?=$item['price']>0?money($item['price']):'—'?></td>
|
||||
<td><?=$item['is_available']?'<span class="badge badge-green">Yes</span>':'<span class="badge badge-gray">No</span>'?></td>
|
||||
<td><?=$item['sort_order']?></td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<button onclick="document.getElementById('eif<?=$item['id']?>').classList.toggle('hidden')" class="btn btn-xs btn-outline">✏️</button>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="del_item"><input type="hidden" name="iid" value="<?=$item['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete item?">🗑️</button></form>
|
||||
</div>
|
||||
<div id="eif<?=$item['id']?>" class="hidden" style="background:var(--bg2);border:1px solid var(--bdr);border-radius:4px;padding:.75rem;margin-top:.4rem;min-width:320px">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="edit_item"><input type="hidden" name="iid" value="<?=$item['id']?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">NAME *</label><input type="text" name="item_name" class="finput" value="<?=e($item['name'])?>" required></div>
|
||||
<div class="fg"><label class="flabel">PRICE</label><input type="number" name="item_price" class="finput" value="<?=$item['price']?>" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" value="<?=e($item['description'])?>"></div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">AVAILABLE</label><select name="is_available" class="fselect"><option value="1"<?=$item['is_available']?' selected':''?>>Yes</option><option value="0"<?=!$item['is_available']?' selected':''?>>No</option></select></div>
|
||||
<div class="fg"><label class="flabel">SORT ORDER</label><input type="number" name="item_order" class="finput" value="<?=$item['sort_order']?>"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success btn-sm">Save Item</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Add item -->
|
||||
<details><summary style="cursor:pointer;font-size:.82rem;color:var(--gold);font-family:'Oswald',sans-serif;letter-spacing:.08em">➕ Add Item to "<?=e($sec['name'])?>"</summary>
|
||||
<div style="padding:.75rem 0">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add_item"><input type="hidden" name="section_id" value="<?=$sec['id']?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">ITEM NAME *</label><input type="text" name="item_name" class="finput" required placeholder="e.g. Spaghetti Carbonara"></div>
|
||||
<div class="fg"><label class="flabel">PRICE ($)</label><input type="number" name="item_price" class="finput" value="0" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" placeholder="Brief description…"></div>
|
||||
<div class="fg"><label class="flabel">AVAILABLE</label><select name="is_available" class="fselect"><option value="1">Yes</option><option value="0">No</option></select></div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add Item</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if(empty($sections)): ?>
|
||||
<div class="empty" style="padding:2rem"><div class="empty-ico">🍽️</div><h3>No menu sections yet</h3><p style="margin-top:.5rem">Use the form above to add the first section.</p></div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<style>.hidden{display:none!important}</style>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
$adminTitle = 'Assign Business Owners';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
if ($act === 'assign') {
|
||||
$uid = (int)p('user_id'); $bid = (int)p('business_id');
|
||||
if ($uid && $bid) {
|
||||
qrun("INSERT OR IGNORE INTO business_owners(user_id,business_id)VALUES(?,?)",[$uid,$bid]);
|
||||
flash('Owner assigned!','success');
|
||||
}
|
||||
}
|
||||
if ($act === 'remove') {
|
||||
qrun("DELETE FROM business_owners WHERE user_id=? AND business_id=?",[(int)p('user_id'),(int)p('business_id')]);
|
||||
flash('Owner removed.','success');
|
||||
}
|
||||
go('/admin/owners.php'.($_GET?'?'.http_build_query($_GET):''));
|
||||
}
|
||||
|
||||
$filterUser = iget('user_id');
|
||||
$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");
|
||||
?>
|
||||
<?php adminTitle('Assign Business Owners','Link user accounts to businesses so they can manage listings, post alerts, and update menus.') ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title">🔑 ASSIGN OWNER TO BUSINESS</div>
|
||||
<form method="POST">
|
||||
<?=csrfField()?><input type="hidden" name="_act" value="assign">
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">USER</label>
|
||||
<select name="user_id" class="fselect" required>
|
||||
<option value="">— Select user —</option>
|
||||
<?php foreach($users as $u): ?><option value="<?=$u['id']?>"<?=$filterUser===$u['id']?' selected':''?>><?=e($u['username'])?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">BUSINESS</label>
|
||||
<select name="business_id" class="fselect" required>
|
||||
<option value="">— Select business —</option>
|
||||
<?php foreach($bizs as $b): ?><option value="<?=$b['id']?>"<?=$filterBiz===$b['id']?' selected':''?>><?=e($b['name'])?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Assign Owner</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-sec-title">CURRENT OWNER ASSIGNMENTS (<?=count($owners)?>)</div>
|
||||
<?php if($owners): ?>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>BUSINESS</th><th>OWNER / USER</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($owners as $o): ?>
|
||||
<tr>
|
||||
<td class="td-name"><a href="/business.php?slug=<?=e(qval("SELECT slug FROM businesses WHERE id=?",[$o['business_id']]))?>" target="_blank"><?=e($o['biz_name'])?></a></td>
|
||||
<td><?=e($o['username'])?></td>
|
||||
<td>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="remove"><input type="hidden" name="user_id" value="<?=$o['user_id']?>"><input type="hidden" name="business_id" value="<?=$o['business_id']?>"><button class="btn btn-xs btn-danger" data-confirm="Remove <?=e($o['username'])?> as owner of <?=e($o['biz_name'])?>?">Remove</button></form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty" style="padding:2rem"><div class="empty-ico">🔑</div><h3>No owner assignments yet</h3></div>
|
||||
<?php endif; ?>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
$adminTitle = 'Listing Reports';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
if ($act === 'close') { qrun("UPDATE reports SET status='closed',admin_notes=? WHERE id=?",[ps('notes'),(int)p('id')]); flash('Report closed.','success'); }
|
||||
if ($act === 'delete') { qrun("DELETE FROM reports WHERE id=?",[(int)p('id')]); flash('Report deleted.','success'); }
|
||||
go('/admin/reports.php');
|
||||
}
|
||||
|
||||
$filter = gs('filter') ?: 'open';
|
||||
$where = $filter === 'all' ? "" : "WHERE r.status='open'";
|
||||
$reports = qall("SELECT r.*,b.name biz,b.slug biz_slug,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 ORDER BY r.created_at DESC");
|
||||
?>
|
||||
<?php adminTitle('Listing Correction Reports','User-submitted corrections and issues for business listings.') ?>
|
||||
|
||||
<div style="display:flex;gap:.4rem;margin-bottom:1rem">
|
||||
<a href="/admin/reports.php?filter=open" class="btn btn-xs<?=$filter==='open'?' btn-primary':' btn-outline'?>">Open</a>
|
||||
<a href="/admin/reports.php?filter=all" class="btn btn-xs<?=$filter==='all'?' btn-primary':' btn-outline'?>">All</a>
|
||||
</div>
|
||||
|
||||
<?php if($reports): ?>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>BUSINESS</th><th>REPORTER</th><th>TYPE</th><th>MESSAGE</th><th>STATUS</th><th>DATE</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($reports as $r): ?>
|
||||
<tr>
|
||||
<td><a href="/business.php?slug=<?=e($r['biz_slug'])?>" target="_blank" class="td-name"><?=e($r['biz'])?></a></td>
|
||||
<td><?=e($r['reporter'])?></td>
|
||||
<td><span class="badge badge-gold"><?=e($r['type'])?></span></td>
|
||||
<td style="font-size:.82rem;max-width:250px"><?=e(substr($r['message'],0,100)).(strlen($r['message'])>100?'…':'')?></td>
|
||||
<td><?=$r['status']==='open'?'<span class="badge badge-red">Open</span>':'<span class="badge badge-green">Closed</span>'?></td>
|
||||
<td style="font-size:.8rem"><?=ago($r['created_at'])?></td>
|
||||
<td>
|
||||
<?php if($r['status']==='open'): ?>
|
||||
<details><summary class="btn btn-xs btn-outline" style="list-style:none;cursor:pointer">Close</summary>
|
||||
<div style="background:var(--bg4);border:1px solid var(--bdr);border-radius:4px;padding:.75rem;margin-top:.4rem;min-width:220px">
|
||||
<div style="font-size:.78rem;color:var(--text-m);margin-bottom:.5rem"><?=e($r['message'])?></div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="close"><input type="hidden" name="id" value="<?=$r['id']?>">
|
||||
<textarea name="notes" class="ftextarea" style="min-height:55px;font-size:.8rem" placeholder="Admin notes (optional)…"></textarea>
|
||||
<button type="submit" class="btn btn-xs btn-success" style="margin-top:.4rem">Mark Closed</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<?php else: ?>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$r['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete report?">🗑️</button></form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty" style="padding:2rem"><div class="empty-ico">✅</div><h3>No <?=$filter==='open'?'open':''?> reports</h3></div>
|
||||
<?php endif; ?>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
$adminTitle = 'Reviews';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
if ($act === 'approve') { qrun("UPDATE reviews SET is_approved=1 WHERE id=?",[(int)p('id')]); flash('Review approved.','success'); }
|
||||
if ($act === 'hide') { qrun("UPDATE reviews SET is_approved=0 WHERE id=?",[(int)p('id')]); flash('Review hidden.','success'); }
|
||||
if ($act === 'delete') { qrun("DELETE FROM reviews WHERE id=?",[(int)p('id')]); flash('Review deleted.','success'); }
|
||||
go('/admin/reviews.php');
|
||||
}
|
||||
|
||||
$page = max(1,iget('page')); $perPage = 30;
|
||||
$total = (int)qval("SELECT COUNT(*) FROM reviews");
|
||||
$pages = max(1,(int)ceil($total/$perPage));
|
||||
$page = min($page,$pages);
|
||||
$offset = ($page-1)*$perPage;
|
||||
$reviews = qall("SELECT r.*,u.username,b.name biz_name,b.slug biz_slug FROM reviews r JOIN users u ON u.id=r.user_id JOIN businesses b ON b.id=r.business_id ORDER BY r.created_at DESC LIMIT $perPage OFFSET $offset");
|
||||
?>
|
||||
<?php adminTitle('Manage Reviews','Approve, hide, or delete user reviews.') ?>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>BUSINESS</th><th>USER</th><th>RATING</th><th>TITLE</th><th>STATUS</th><th>DATE</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($reviews as $r): ?>
|
||||
<tr>
|
||||
<td><a href="/business.php?slug=<?=e($r['biz_slug'])?>" target="_blank" class="td-name"><?=e($r['biz_name'])?></a></td>
|
||||
<td><?=e($r['username'])?></td>
|
||||
<td><?=starDisplay((float)$r['rating'])?></td>
|
||||
<td style="font-size:.82rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?=e($r['title']?:$r['body'])?></td>
|
||||
<td><?=$r['is_approved']?'<span class="badge badge-green">Visible</span>':'<span class="badge badge-gray">Hidden</span>'?></td>
|
||||
<td style="font-size:.8rem"><?=ago($r['created_at'])?></td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<?php if(!$r['is_approved']): ?><form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="approve"><input type="hidden" name="id" value="<?=$r['id']?>"><button class="btn btn-xs btn-success">Approve</button></form>
|
||||
<?php else: ?><form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="hide"><input type="hidden" name="id" value="<?=$r['id']?>"><button class="btn btn-xs btn-secondary">Hide</button></form><?php endif; ?>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$r['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete this review?">🗑️</button></form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if($pages>1): ?>
|
||||
<div style="display:flex;gap:.5rem;margin-top:1rem;flex-wrap:wrap"><?php for($i=1;$i<=$pages;$i++): ?><a href="?page=<?=$i?>" class="btn btn-sm<?=$i===$page?' btn-primary':' btn-outline'?>"><?=$i?></a><?php endfor; ?></div>
|
||||
<?php endif; ?>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
$adminTitle = 'Site Settings';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'settings') {
|
||||
setSetting('site_name', ps('site_name'));
|
||||
setSetting('site_tagline', ps('site_tagline'));
|
||||
setSetting('contact_email', ps('contact_email'));
|
||||
setSetting('registration_enabled',p('registration_enabled','0'));
|
||||
setSetting('events_require_approval',p('events_require_approval','0'));
|
||||
flash('Settings saved!','success');
|
||||
go('/admin/settings.php');
|
||||
}
|
||||
|
||||
// Change admin password
|
||||
if ($act === 'change_password') {
|
||||
$pw = ps('new_password');
|
||||
$pw2 = ps('confirm_password');
|
||||
if (strlen($pw) < 6) { flash('Password must be at least 6 characters.','error'); go('/admin/settings.php'); }
|
||||
if ($pw !== $pw2) { flash('Passwords do not match.','error'); go('/admin/settings.php'); }
|
||||
qrun("UPDATE users SET password=? WHERE id=?",[password_hash($pw,PASSWORD_BCRYPT),uid()]);
|
||||
flash('Password updated successfully!','success');
|
||||
go('/admin/settings.php');
|
||||
}
|
||||
}
|
||||
|
||||
$regEnabled = setting('registration_enabled','1');
|
||||
$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');
|
||||
$me = qone("SELECT * FROM users WHERE id=?",[uid()]);
|
||||
?>
|
||||
<?php adminTitle('Site Settings') ?>
|
||||
|
||||
<div class="g2" style="gap:1.5rem">
|
||||
<!-- General settings -->
|
||||
<div>
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">🌐 GENERAL SETTINGS</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="settings">
|
||||
<div class="fg"><label class="flabel">SITE NAME</label><input type="text" name="site_name" class="finput" value="<?=e($siteName)?>"></div>
|
||||
<div class="fg"><label class="flabel">SITE TAGLINE</label><input type="text" name="site_tagline" class="finput" value="<?=e($siteTagline)?>"></div>
|
||||
<div class="fg"><label class="flabel">CONTACT EMAIL</label><input type="email" name="contact_email" class="finput" value="<?=e($contactEmail)?>"></div>
|
||||
<hr class="divider" style="margin:1rem 0">
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">USER REGISTRATION</label>
|
||||
<div style="display:flex;gap:1rem;margin-top:.4rem">
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.9rem">
|
||||
<input type="radio" name="registration_enabled" value="1"<?=$regEnabled==='1'?' checked':''?>>
|
||||
<span style="color:var(--green-l)">✅ Enabled — users can register</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:1rem;margin-top:.4rem">
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.9rem">
|
||||
<input type="radio" name="registration_enabled" value="0"<?=$regEnabled==='0'?' checked':''?>>
|
||||
<span style="color:var(--red-l)">🔒 Disabled — registration closed</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="fhint">When disabled, the Sign Up link is hidden and registration page shows a message.</div>
|
||||
</div>
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">EVENT SUBMISSIONS</label>
|
||||
<div style="margin-top:.4rem">
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.9rem;margin-bottom:.35rem">
|
||||
<input type="radio" name="events_require_approval" value="1"<?=$evtApproval==='1'?' checked':''?>>
|
||||
<span>Require admin approval before events go public</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.9rem">
|
||||
<input type="radio" name="events_require_approval" value="0"<?=$evtApproval==='0'?' checked':''?>>
|
||||
<span>Auto-approve all submitted events</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin password change -->
|
||||
<div>
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">🔑 CHANGE YOUR ADMIN PASSWORD</div>
|
||||
<div class="irow"><span class="irow-i">👤</span><span class="irow-v"><?=e($me['username'])?> <span class="badge badge-gold">ADMIN</span></span></div>
|
||||
<div class="irow" style="margin-bottom:1rem"><span class="irow-i">📅</span><span class="irow-v">Last login: <?=$me['last_login']?ago($me['last_login']):'—'?></span></div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="change_password">
|
||||
<div class="fg"><label class="flabel">NEW PASSWORD *</label><input type="password" name="new_password" class="finput" required autocomplete="new-password"><div class="fhint">Minimum 6 characters.</div></div>
|
||||
<div class="fg"><label class="flabel">CONFIRM NEW PASSWORD *</label><input type="password" name="confirm_password" class="finput" required autocomplete="new-password"></div>
|
||||
<button type="submit" class="btn btn-primary">Update Password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- DB info -->
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">📊 DATABASE INFO</div>
|
||||
<?php
|
||||
$counts = [
|
||||
'Businesses' => qval("SELECT COUNT(*) FROM businesses"),
|
||||
'Users' => qval("SELECT COUNT(*) FROM users"),
|
||||
'Reviews' => qval("SELECT COUNT(*) FROM reviews"),
|
||||
'Events' => qval("SELECT COUNT(*) FROM events"),
|
||||
'Attractions'=> qval("SELECT COUNT(*) FROM attractions"),
|
||||
'Alerts' => qval("SELECT COUNT(*) FROM alerts"),
|
||||
'Reports' => qval("SELECT COUNT(*) FROM reports"),
|
||||
'Menu Sections'=> qval("SELECT COUNT(*) FROM menu_sections"),
|
||||
'Menu Items' => qval("SELECT COUNT(*) FROM menu_items"),
|
||||
];
|
||||
foreach ($counts as $lbl => $n):
|
||||
?><div class="irow"><span class="irow-i">📌</span><span class="irow-v" style="display:flex;justify-content:space-between;width:100%"><span><?=e($lbl)?></span><strong style="color:var(--gold)"><?=$n?></strong></span></div><?php endforeach; ?>
|
||||
<?php if(file_exists(DB_FILE)): ?>
|
||||
<div class="irow"><span class="irow-i">💾</span><span class="irow-v" style="display:flex;justify-content:space-between;width:100%"><span>DB File Size</span><strong style="color:var(--gold)"><?=number_format(filesize(DB_FILE)/1024,1)?> KB</strong></span></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
$adminTitle = 'Users';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'add') {
|
||||
$un = ps('username'); $em = ps('email'); $pw = ps('password');
|
||||
if (!$un || !$pw) { flash('Username and password required.','error'); go('/admin/users.php'); }
|
||||
if (qval("SELECT id FROM users WHERE username=?",[$un])) { flash('Username taken.','error'); go('/admin/users.php'); }
|
||||
qrun("INSERT INTO users(username,email,password,is_admin,is_active)VALUES(?,?,?,?,?)",[$un,$em?:null,password_hash($pw,PASSWORD_BCRYPT),(int)p('is_admin',0),(int)p('is_active',1)]);
|
||||
flash('User added!','success'); go('/admin/users.php');
|
||||
}
|
||||
if ($act === 'edit') {
|
||||
$id = (int)p('id');
|
||||
$un = ps('username'); $em = ps('email'); $pw = ps('password');
|
||||
if (!$un) { flash('Username required.','error'); go('/admin/users.php'); }
|
||||
if (qval("SELECT id FROM users WHERE username=? AND id!=?",[$un,$id])) { flash('Username taken.','error'); go('/admin/users.php'); }
|
||||
if ($pw) {
|
||||
qrun("UPDATE users SET username=?,email=?,password=?,is_admin=?,is_active=? WHERE id=?",[$un,$em?:null,password_hash($pw,PASSWORD_BCRYPT),(int)p('is_admin',0),(int)p('is_active',1),$id]);
|
||||
} else {
|
||||
qrun("UPDATE users SET username=?,email=?,is_admin=?,is_active=? WHERE id=?",[$un,$em?:null,(int)p('is_admin',0),(int)p('is_active',1),$id]);
|
||||
}
|
||||
flash('User updated!','success'); go('/admin/users.php');
|
||||
}
|
||||
if ($act === 'delete') {
|
||||
$id = (int)p('id');
|
||||
if ($id === uid()) { flash('You cannot delete your own account.','error'); go('/admin/users.php'); }
|
||||
qrun("DELETE FROM users WHERE id=?",[$id]);
|
||||
flash('User deleted.','success'); go('/admin/users.php');
|
||||
}
|
||||
if ($act === 'toggle_active') {
|
||||
$id = (int)p('id');
|
||||
if ($id !== uid()) { qrun("UPDATE users SET is_active=1-is_active WHERE id=?",[$id]); }
|
||||
go('/admin/users.php');
|
||||
}
|
||||
}
|
||||
|
||||
$editId = iget('edit');
|
||||
$editing = $editId ? qone("SELECT * FROM users WHERE id=?",[$editId]) : null;
|
||||
$page = max(1,iget('page')); $perPage = 30;
|
||||
$total = (int)qval("SELECT COUNT(*) FROM users");
|
||||
$pages = max(1,(int)ceil($total/$perPage));
|
||||
$page = min($page,$pages);
|
||||
$offset = ($page-1)*$perPage;
|
||||
$users = qall("SELECT * FROM users ORDER BY is_admin DESC,username ASC LIMIT $perPage OFFSET $offset");
|
||||
?>
|
||||
<?php adminTitle($editing ? 'Edit User: '.e($editing['username']) : 'Manage Users') ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title"><?=$editing ? '✏️ EDIT USER: '.strtoupper(e($editing['username'])) : '➕ ADD NEW USER'?></div>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="<?=$editing?'edit':'add'?>">
|
||||
<?php if($editing): ?><input type="hidden" name="id" value="<?=$editing['id']?>"><?php endif; ?>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">USERNAME *</label><input type="text" name="username" class="finput" value="<?=e($editing['username']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">EMAIL</label><input type="email" name="email" class="finput" value="<?=e($editing['email']??'')?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel"><?=$editing?'NEW PASSWORD (leave blank to keep)':'PASSWORD *'?></label><input type="password" name="password" class="finput" <?=$editing?'':'required'?>><div class="fhint">Minimum 6 characters.</div></div>
|
||||
<div class="fg" style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
|
||||
<div class="fg"><label class="flabel">ROLE</label><select name="is_admin" class="fselect"><option value="0"<?=!($editing['is_admin']??0)?' selected':''?>>Regular User</option><option value="1"<?=($editing['is_admin']??0)?' selected':''?>>Administrator</option></select></div>
|
||||
<div class="fg"><label class="flabel">STATUS</label><select name="is_active" class="fselect"><option value="1"<?=($editing['is_active']??1)?' selected':''?>>Active</option><option value="0"<?=isset($editing)&&!$editing['is_active']?' selected':''?>>Suspended</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary"><?=$editing?'Update User':'Add User'?></button>
|
||||
<?php if($editing): ?><a href="/admin/users.php" class="btn btn-secondary">Cancel</a><?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-sec-title">ALL USERS (<?=$total?>)</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>USERNAME</th><th>EMAIL</th><th>ROLE</th><th>STATUS</th><th>JOINED</th><th>LAST LOGIN</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($users as $u): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($u['username'])?></td>
|
||||
<td style="font-size:.8rem"><?=e($u['email']??'—')?></td>
|
||||
<td><?=$u['is_admin']?'<span class="badge badge-gold">ADMIN</span>':'<span class="badge badge-gray">User</span>'?></td>
|
||||
<td><?=$u['is_active']?'<span class="badge badge-green">Active</span>':'<span class="badge badge-red">Suspended</span>'?></td>
|
||||
<td style="font-size:.8rem"><?=fdate($u['created_at'])?></td>
|
||||
<td style="font-size:.8rem"><?=$u['last_login']?ago($u['last_login']):'Never'?></td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<a href="/admin/users.php?edit=<?=$u['id']?>" class="btn btn-xs btn-outline">✏️ Edit</a>
|
||||
<a href="/admin/owners.php?user_id=<?=$u['id']?>" class="btn btn-xs btn-secondary">🔑 Assign</a>
|
||||
<?php if($u['id']!==uid()): ?>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="toggle_active"><input type="hidden" name="id" value="<?=$u['id']?>"><button class="btn btn-xs btn-secondary"><?=$u['is_active']?'Suspend':'Restore'?></button></form>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete"><input type="hidden" name="id" value="<?=$u['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete user '<?=e($u['username'])?>'?">🗑️</button></form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if($pages>1): ?>
|
||||
<div style="display:flex;gap:.5rem;margin-top:1rem;flex-wrap:wrap">
|
||||
<?php for($i=1;$i<=$pages;$i++): ?><a href="?page=<?=$i?>" class="btn btn-sm<?=$i===$page?' btn-primary':' btn-outline'?>"><?=$i?></a><?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -0,0 +1,336 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
DISCOVER KEYSER WV — Dark West Virginia Theme
|
||||
WV Gold #c9a84c · Navy #1a3a5c · Near-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);
|
||||
--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}
|
||||
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}
|
||||
h4,h5,h6,.lbl{font-family:'Oswald',sans-serif;letter-spacing:.06em}
|
||||
ul{list-style:none} img{max-width:100%;display:block}
|
||||
button{cursor:pointer;font-family:inherit} input,select,textarea{font-family:inherit}
|
||||
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)}
|
||||
.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))}
|
||||
.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-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-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}
|
||||
.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)}
|
||||
.user-dd{display:none;position:absolute;right:0;top:calc(100%+6px);background:var(--bg3);border:1px solid var(--bdr-l);border-radius:var(--r);min-width:175px;box-shadow:var(--sh);z-index:200;padding:.4rem 0}
|
||||
.user-dd.open{display:block}
|
||||
.user-dd a{display:block;padding:.55rem 1rem;font-size:.88rem;color:var(--text-m);transition:all var(--t)}
|
||||
.user-dd a:hover{background:var(--gold-dim);color:var(--gold)}
|
||||
.user-dd hr{border:none;border-top:1px solid var(--bdr);margin:.3rem 0}
|
||||
.hamburger{display:none;flex-direction:column;gap:5px;background:none;border:none;padding:.4rem}
|
||||
.hamburger span{display:block;width:24px;height:2px;background:var(--text-m);border-radius:2px;transition:all .25s}
|
||||
|
||||
/* ── Flash ─────────────────────────────────────────────── */
|
||||
.flash{display:flex;align-items:center;justify-content:center;gap:.75rem;padding:.8rem 1.5rem;font-size:.88rem}
|
||||
.flash button{background:none;border:none;color:inherit;opacity:.6;font-size:.9rem;padding:.1rem .3rem}
|
||||
.flash button:hover{opacity:1}
|
||||
.f-info {background:rgba(26,58,92,.4);border-bottom:1px solid var(--navy-m);color:var(--gold-l)}
|
||||
.f-success{background:rgba(42,122,71,.28);border-bottom:1px solid var(--green-l);color:#86e8a8}
|
||||
.f-error {background:rgba(143,45,45,.32);border-bottom:1px solid var(--red-l);color:#ff9a9a}
|
||||
.f-warning{background:rgba(179,120,32,.22);border-bottom:1px solid var(--amber);color:#ffc96a}
|
||||
|
||||
/* ── 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-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%}
|
||||
.hero-content{position:relative;z-index:2;text-align:center;max-width:900px;padding:2rem 1.5rem}
|
||||
.hero-eyebrow{display:inline-flex;align-items:center;gap:.5rem;background:rgba(201,168,76,.08);border:1px solid var(--gold-dim);padding:.32rem 1.4rem;border-radius:2px;font-family:'Oswald',sans-serif;font-size:.68rem;letter-spacing:.35em;color:var(--gold);margin-bottom:1.5rem;animation:fadeDown .6s ease both}
|
||||
.hero-h1{font-size:clamp(3.2rem,9vw,6.8rem);font-weight:900;color:var(--text);line-height:.92;text-shadow:0 4px 50px rgba(0,0,0,.95);animation:fadeUp .65s ease .1s both}
|
||||
.hero-h1 .gld{color:var(--gold);display:block}
|
||||
.hero-sub{font-family:'Oswald',sans-serif;font-size:clamp(.8rem,1.8vw,1.05rem);letter-spacing:.4em;color:var(--text-m);margin-top:.8rem;animation:fadeUp .65s ease .2s both}
|
||||
.hero-rule{width:60px;height:2px;background:linear-gradient(90deg,transparent,var(--gold),transparent);margin:1.4rem auto;animation:fadeIn .65s ease .25s both}
|
||||
.hero-desc{font-size:1.08rem;color:var(--text-m);max-width:600px;margin:0 auto;line-height:1.85;animation:fadeUp .65s ease .3s both}
|
||||
.hero-actions{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap;margin-top:2.25rem;animation:fadeUp .65s ease .4s both}
|
||||
|
||||
/* ── 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-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-navy{background:var(--navy-m);color:#fff}
|
||||
.btn-navy:hover{background:var(--navy-l)}
|
||||
.btn-danger{background:var(--red);color:#fff}
|
||||
.btn-danger:hover{background:var(--red-l)}
|
||||
.btn-success{background:var(--green);color:#fff}
|
||||
.btn-success:hover{background:var(--green-l)}
|
||||
.btn-sm{padding:.38rem 1rem;font-size:.74rem}
|
||||
.btn-xs{padding:.22rem .6rem;font-size:.68rem}
|
||||
.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-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}
|
||||
.stat-l{font-family:'Oswald',sans-serif;font-size:.66rem;letter-spacing:.22em;color:var(--text-d);margin-top:.2rem}
|
||||
|
||||
/* ── Layout ─────────────────────────────────────────────── */
|
||||
.page-hdr{background:var(--bg2);padding:3rem 1.5rem 2rem;border-bottom:1px solid var(--bdr)}
|
||||
.page-hdr-inner{max-width:1440px;margin:0 auto}
|
||||
.section{max-width:1440px;margin:0 auto;padding:4rem 1.5rem}
|
||||
.section-sm{max-width:960px;margin:0 auto;padding:3rem 1.5rem}
|
||||
.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}
|
||||
|
||||
/* ── Grids ──────────────────────────────────────────────── */
|
||||
.g3{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1.4rem}
|
||||
.g4{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1.2rem}
|
||||
.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-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}
|
||||
|
||||
/* ── Stars ──────────────────────────────────────────────── */
|
||||
.stars{display:inline-flex;gap:1px}
|
||||
.star{font-size:.88rem;color:var(--text-d)}
|
||||
.star.on{color:var(--gold)}
|
||||
.rat-score{font-family:'Oswald',sans-serif;font-size:1rem;color:var(--gold)}
|
||||
.rat-cnt{font-size:.78rem;color:var(--text-d)}
|
||||
.star-picker{display:inline-flex;flex-direction:row-reverse;gap:.2rem}
|
||||
.star-picker input{display:none}
|
||||
.star-picker label{font-size:1.6rem;color:var(--text-d);cursor:pointer;transition:color .1s}
|
||||
.star-picker input:checked~label,.star-picker label:hover,.star-picker label:hover~label{color:var(--gold)}
|
||||
|
||||
/* ── Info box ───────────────────────────────────────────── */
|
||||
.ibox{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.4rem;margin-bottom:1.4rem}
|
||||
.ibox-title{font-family:'Oswald',sans-serif;letter-spacing:.1em;font-size:.88rem;color:var(--gold);margin-bottom:.9rem;padding-bottom:.6rem;border-bottom:1px solid var(--bdr)}
|
||||
.irow{display:flex;gap:.6rem;align-items:flex-start;padding:.42rem 0;border-bottom:1px solid var(--bdr);font-size:.84rem}
|
||||
.irow:last-child{border-bottom:none}
|
||||
.irow-i{color:var(--gold);flex-shrink:0;width:18px;text-align:center;margin-top:1px}
|
||||
.irow-v{color:var(--text)}
|
||||
|
||||
/* ── Hours ──────────────────────────────────────────────── */
|
||||
.hrs-grid{display:grid;grid-template-columns:1fr 1fr;gap:.22rem}
|
||||
.hrs-row{display:flex;justify-content:space-between;padding:.28rem .42rem;font-size:.8rem;border-radius:3px}
|
||||
.hrs-row:nth-child(odd){background:rgba(255,255,255,.02)}
|
||||
.hrs-day{font-family:'Oswald',sans-serif;font-size:.72rem;letter-spacing:.06em;color:var(--text-d)}
|
||||
.hrs-time{color:var(--text)}
|
||||
|
||||
/* ── Alerts ─────────────────────────────────────────────── */
|
||||
.alert-strip{background:var(--bg2);border-bottom:1px solid var(--bdr);padding:2rem 1.5rem}
|
||||
.alert-strip-inner{max-width:1440px;margin:0 auto}
|
||||
.alert-item{display:flex;gap:.85rem;align-items:flex-start;padding:.9rem 1.1rem;border-radius:var(--r);margin-bottom:.65rem}
|
||||
.alert-item:last-child{margin-bottom:0}
|
||||
.a-info {background:rgba(26,72,118,.22);border:1px solid rgba(42,95,153,.35)}
|
||||
.a-news {background:rgba(42,122,71,.16);border:1px solid rgba(58,165,94,.3)}
|
||||
.a-event {background:rgba(201,168,76,.1);border:1px solid rgba(201,168,76,.28)}
|
||||
.a-warning{background:rgba(183,120,32,.16);border:1px solid rgba(183,120,32,.35)}
|
||||
.a-sale {background:rgba(120,45,140,.16);border:1px solid rgba(150,65,175,.3)}
|
||||
.alert-ico{font-size:1.05rem;flex-shrink:0;margin-top:2px}
|
||||
.alert-title{font-family:'Oswald',sans-serif;letter-spacing:.05em;font-size:.92rem;color:var(--text)}
|
||||
.alert-body{font-size:.84rem;color:var(--text-m);margin-top:.12rem;line-height:1.55}
|
||||
.alert-meta{font-size:.73rem;color:var(--text-d);margin-top:.22rem}
|
||||
|
||||
/* ── Detail layout ──────────────────────────────────────── */
|
||||
.detail-hdr{background:linear-gradient(180deg,var(--bg2),var(--bg1));padding:3rem 1.5rem 2rem;border-bottom:1px solid var(--bdr)}
|
||||
.detail-hdr-inner{max-width:1280px;margin:0 auto}
|
||||
.breadcrumb{font-size:.78rem;color:var(--text-d);margin-bottom:1rem}
|
||||
.breadcrumb a{color:var(--text-m)}.breadcrumb a:hover{color:var(--gold)}
|
||||
.breadcrumb span{margin:0 .4rem;color:var(--text-d)}
|
||||
.detail-body{max-width:1280px;margin:0 auto;padding:2.5rem 1.5rem;display:grid;grid-template-columns:1fr 330px;gap:2.5rem}
|
||||
|
||||
/* ── Menu ───────────────────────────────────────────────── */
|
||||
.menu-sec{margin-bottom:2rem}
|
||||
.menu-sec-name{font-family:'Oswald',sans-serif;font-size:.92rem;letter-spacing:.14em;color:var(--gold);border-bottom:1px solid rgba(201,168,76,.25);padding-bottom:.45rem;margin-bottom:.9rem}
|
||||
.menu-sec-desc{font-size:.8rem;color:var(--text-d);margin-bottom:.6rem;font-style:italic}
|
||||
.menu-item{display:flex;justify-content:space-between;align-items:flex-start;padding:.65rem 0;border-bottom:1px solid var(--bdr)}
|
||||
.menu-item:last-child{border-bottom:none}
|
||||
.mi-info{flex:1;margin-right:.75rem}
|
||||
.mi-name{font-weight:600;font-size:.92rem;color:var(--text)}
|
||||
.mi-desc{font-size:.78rem;color:var(--text-d);margin-top:.12rem}
|
||||
.mi-price{font-family:'Oswald',sans-serif;font-size:.95rem;color:var(--gold);flex-shrink:0}
|
||||
|
||||
/* ── Reviews ────────────────────────────────────────────── */
|
||||
.rev-card{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.2rem;margin-bottom:.9rem}
|
||||
.rev-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.65rem}
|
||||
.rev-author{font-family:'Oswald',sans-serif;font-size:.88rem;letter-spacing:.05em;color:var(--text)}
|
||||
.rev-date{font-size:.73rem;color:var(--text-d)}
|
||||
.rev-title{font-size:.9rem;font-weight:600;color:var(--text);margin:.4rem 0 .3rem}
|
||||
.rev-body{font-size:.88rem;color:var(--text-m);line-height:1.62}
|
||||
|
||||
/* ── Forms ──────────────────────────────────────────────── */
|
||||
.form-box{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r-lg);padding:2.25rem;max-width:480px;margin:4rem auto}
|
||||
.form-title{font-size:2rem;margin-bottom:.25rem}
|
||||
.form-sub{color:var(--text-d);font-size:.88rem;margin-bottom:1.75rem}
|
||||
.fg{margin-bottom:1.15rem}
|
||||
.flabel{display:block;font-family:'Oswald',sans-serif;letter-spacing:.1em;font-size:.72rem;color:var(--gold);margin-bottom:.42rem}
|
||||
.finput,.fselect,.ftextarea{width:100%;padding:.7rem 1rem;background:rgba(0,0,0,.4);border:1px solid var(--bdr-l);border-radius:var(--r);color:var(--text);font-size:.92rem;transition:border-color var(--t)}
|
||||
.finput:focus,.fselect:focus,.ftextarea:focus{outline:none;border-color:var(--gold-d)}
|
||||
.finput::placeholder,.ftextarea::placeholder{color:var(--text-d)}
|
||||
.fselect option{background:var(--bg3);color:var(--text)}
|
||||
.ftextarea{resize:vertical;min-height:100px;line-height:1.6}
|
||||
.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}
|
||||
|
||||
/* ── Search ─────────────────────────────────────────────── */
|
||||
.search-bar{background:var(--bg2);padding:1.75rem 1.5rem;border-bottom:1px solid var(--bdr)}
|
||||
.search-bar-inner{max-width:860px;margin:0 auto}
|
||||
.search-form{display:flex;gap:.65rem;flex-wrap:wrap}
|
||||
.search-wrap{flex:1;min-width:200px;position:relative}
|
||||
.s-icon{position:absolute;left:.9rem;top:50%;transform:translateY(-50%);color:var(--text-d)}
|
||||
.search-input{width:100%;padding:.72rem .9rem .72rem 2.5rem;background:rgba(0,0,0,.4);border:1px solid var(--bdr-l);border-radius:var(--r);color:var(--text);font-size:.92rem}
|
||||
.search-input:focus{outline:none;border-color:var(--gold-d)}
|
||||
.search-input::placeholder{color:var(--text-d)}
|
||||
.filter-bar{background:var(--bg1);border-bottom:1px solid var(--bdr);padding:.6rem 1.5rem;overflow-x:auto}
|
||||
.filter-bar-inner{max-width:1440px;margin:0 auto;display:flex;gap:.45rem;flex-wrap:nowrap}
|
||||
|
||||
/* ── Tables ─────────────────────────────────────────────── */
|
||||
.tbl-wrap{overflow-x:auto;border-radius:var(--r);border:1px solid var(--bdr)}
|
||||
.dtbl{width:100%;border-collapse:collapse;font-size:.86rem}
|
||||
.dtbl th{background:var(--bg4);color:var(--gold);font-family:'Oswald',sans-serif;letter-spacing:.1em;font-size:.72rem;padding:.72rem 1rem;text-align:left;border-bottom:2px solid rgba(201,168,76,.25);white-space:nowrap}
|
||||
.dtbl td{padding:.72rem 1rem;border-bottom:1px solid var(--bdr);color:var(--text-m);vertical-align:middle}
|
||||
.dtbl tr:last-child td{border-bottom:none}
|
||||
.dtbl tr:hover td{background:rgba(255,255,255,.02)}
|
||||
.td-name{color:var(--text);font-weight:500}
|
||||
|
||||
/* ── Badges ─────────────────────────────────────────────── */
|
||||
.badge{display:inline-block;padding:.14rem .5rem;border-radius:3px;font-family:'Oswald',sans-serif;font-size:.68rem;letter-spacing:.08em}
|
||||
.badge-gold {background:rgba(201,168,76,.15);color:var(--gold);border:1px solid rgba(201,168,76,.28)}
|
||||
.badge-green{background:rgba(42,122,71,.22);color:#86e8a8;border:1px solid rgba(58,165,94,.35)}
|
||||
.badge-red {background:rgba(143,45,45,.28);color:#ff9a9a;border:1px solid rgba(192,64,64,.4)}
|
||||
.badge-blue {background:rgba(26,72,118,.3);color:#88c0ec;border:1px solid rgba(42,96,153,.4)}
|
||||
.badge-gray {background:rgba(88,104,126,.2);color:var(--text-d);border:1px solid var(--bdr-l)}
|
||||
|
||||
/* ── Admin layout ───────────────────────────────────────── */
|
||||
.admin-wrap{display:grid;grid-template-columns:220px 1fr;min-height:calc(100vh - 64px)}
|
||||
.admin-side{background:var(--bg2);border-right:1px solid var(--bdr);padding:1.75rem 1.1rem;position:sticky;top:64px;align-self:start;max-height:calc(100vh - 64px);overflow-y:auto}
|
||||
.admin-side-title{font-family:'Oswald',sans-serif;font-size:.68rem;letter-spacing:.28em;color:var(--gold);margin-bottom:.85rem;padding-bottom:.5rem;border-bottom:1px solid var(--bdr)}
|
||||
.asl{display:block;padding:.55rem .75rem;border-radius:4px;font-size:.87rem;color:var(--text-m);margin-bottom:.18rem;transition:all var(--t)}
|
||||
.asl:hover,.asl.on{background:var(--gold-dim);color:var(--gold)}
|
||||
.admin-main{padding:2.25rem;overflow-x:auto}
|
||||
.admin-sec-title{font-family:'Oswald',sans-serif;font-size:1.05rem;letter-spacing:.1em;color:var(--text);margin-bottom:1.15rem;padding-bottom:.65rem;border-bottom:1px solid var(--bdr)}
|
||||
.stat-cards{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
||||
.stat-card{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.25rem 1.65rem;flex:1;min-width:130px;text-align:center}
|
||||
.sc-n{font-family:'Playfair Display',serif;font-size:2.2rem;color:var(--gold);line-height:1}
|
||||
.sc-l{font-family:'Oswald',sans-serif;font-size:.66rem;letter-spacing:.18em;color:var(--text-d);margin-top:.22rem}
|
||||
|
||||
/* ── Events ─────────────────────────────────────────────── */
|
||||
.evt-card{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.5rem;display:flex;gap:1.5rem;transition:all .22s;margin-bottom:1.1rem}
|
||||
.evt-card:hover{border-color:var(--gold-d);box-shadow:var(--sh-sm)}
|
||||
.evt-date-box{flex-shrink:0;text-align:center;background:var(--bg4);border:1px solid var(--bdr-l);border-radius:var(--r);padding:.65rem .85rem;min-width:64px}
|
||||
.evt-mon{font-family:'Oswald',sans-serif;font-size:.68rem;letter-spacing:.18em;color:var(--gold)}
|
||||
.evt-day{font-family:'Playfair Display',serif;font-size:2rem;color:var(--text);line-height:1}
|
||||
.evt-yr{font-family:'Oswald',sans-serif;font-size:.65rem;color:var(--text-d);margin-top:2px}
|
||||
.evt-body{flex:1;min-width:0}
|
||||
.evt-title{font-size:1.18rem;font-weight:700;color:var(--text);margin-bottom:.35rem}
|
||||
.evt-meta{display:flex;gap:.85rem;flex-wrap:wrap;font-size:.8rem;color:var(--text-d);margin-bottom:.5rem}
|
||||
.evt-meta span{display:flex;align-items:center;gap:.3rem}
|
||||
.evt-desc{font-size:.88rem;color:var(--text-m);line-height:1.62}
|
||||
.evt-actions{margin-top:.75rem;display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
|
||||
/* ── About / Facts ──────────────────────────────────────── */
|
||||
.fact-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:1.1rem}
|
||||
.fact-card{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.4rem}
|
||||
.fact-icon{font-size:2rem;margin-bottom:.6rem}
|
||||
.fact-lbl{font-family:'Oswald',sans-serif;font-size:.72rem;letter-spacing:.18em;color:var(--gold)}
|
||||
.fact-val{font-size:.95rem;color:var(--text);margin-top:.3rem}
|
||||
.prose p{color:var(--text-m);line-height:1.9;margin-bottom:1.1rem}
|
||||
.prose strong{color:var(--gold)}
|
||||
.timeline{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.4rem}
|
||||
.tl-item{display:flex;gap:.8rem;padding:.48rem 0;border-bottom:1px solid var(--bdr);font-size:.82rem}
|
||||
.tl-item:last-child{border-bottom:none}
|
||||
.tl-yr{font-family:'Oswald',sans-serif;color:var(--gold);min-width:52px;flex-shrink:0}
|
||||
.tl-evt{color:var(--text-m)}
|
||||
.famous-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem}
|
||||
.famous-card{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.3rem}
|
||||
.famous-name{font-family:'Playfair Display',serif;font-size:1.08rem;color:var(--text);margin-bottom:.2rem}
|
||||
.famous-yrs{font-size:.74rem;color:var(--text-d);margin-bottom:.45rem}
|
||||
.famous-bio{font-size:.83rem;color:var(--text-m);line-height:1.6}
|
||||
|
||||
/* ── Attractions ────────────────────────────────────────── */
|
||||
.attr-card{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.4rem;transition:all .22s}
|
||||
.attr-card:hover{border-color:var(--gold-d);transform:translateY(-2px);box-shadow:var(--sh-sm)}
|
||||
.attr-cat{font-family:'Oswald',sans-serif;font-size:.62rem;letter-spacing:.2em;color:var(--gold);margin-bottom:.4rem}
|
||||
.attr-name{font-size:1.1rem;font-weight:700;color:var(--text);margin-bottom:.6rem}
|
||||
.attr-desc{font-size:.85rem;color:var(--text-m);line-height:1.65}
|
||||
.attr-det{display:flex;gap:.45rem;font-size:.78rem;color:var(--text-d);margin-top:.55rem;align-items:flex-start}
|
||||
|
||||
/* ── Footer ─────────────────────────────────────────────── */
|
||||
.footer{background:var(--bg2);border-top:1px solid var(--bdr);margin-top:auto}
|
||||
.footer-grid{max-width:1440px;margin:0 auto;padding:3rem 1.5rem;display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:2.5rem}
|
||||
.f-logo{font-family:'Oswald',sans-serif;font-size:1.25rem;letter-spacing:.18em;color:var(--gold);margin-bottom:.5rem}
|
||||
.f-tag{font-style:italic;color:rgba(201,168,76,.7);font-size:.88rem;margin-bottom:.75rem}
|
||||
.footer p{font-size:.83rem;color:var(--text-d);line-height:1.7}
|
||||
.footer h5{font-family:'Oswald',sans-serif;font-size:.76rem;letter-spacing:.16em;color:var(--gold);margin-bottom:.75rem}
|
||||
.footer ul li{margin-bottom:.35rem}
|
||||
.footer ul li a,.footer address a{font-size:.83rem;color:var(--text-d)}
|
||||
.footer ul li a:hover,.footer address a:hover{color:var(--gold)}
|
||||
.footer address p{margin-bottom:.35rem}
|
||||
.footer-bar{border-top:1px solid var(--bdr);padding:1.2rem 1.5rem;text-align:center}
|
||||
.footer-bar p{font-size:.78rem;color:var(--text-d)}
|
||||
|
||||
/* ── Utilities ──────────────────────────────────────────── */
|
||||
.divider{border:none;border-top:1px solid var(--bdr);margin:2rem 0}
|
||||
.empty{text-align:center;padding:4rem 2rem;color:var(--text-d)}
|
||||
.empty-ico{font-size:3.5rem;margin-bottom:1rem}
|
||||
.empty h3{color:var(--text-m);font-family:'Oswald',sans-serif;font-weight:400;font-size:1.1rem;letter-spacing:.1em}
|
||||
.flex-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
|
||||
.mt1{margin-top:.5rem}.mt2{margin-top:1rem}.mt3{margin-top:1.5rem}
|
||||
.mb1{margin-bottom:.5rem}.mb2{margin-bottom:1rem}
|
||||
.text-gold{color:var(--gold)}.text-muted{color:var(--text-m)}.text-dim{color:var(--text-d)}
|
||||
.text-right{text-align:right}.text-center{text-align:center}
|
||||
|
||||
/* ── Animations ─────────────────────────────────────────── */
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes fadeDown{from{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}
|
||||
@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: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}
|
||||
}
|
||||
@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}}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Nav hamburger
|
||||
const ham=document.getElementById('ham'),navLinks=document.getElementById('navLinks');
|
||||
ham?.addEventListener('click',()=>navLinks?.classList.toggle('open'));
|
||||
document.addEventListener('click',e=>{if(!e.target.closest('.navbar'))navLinks?.classList.remove('open')});
|
||||
|
||||
// User dropdown
|
||||
const uBtn=document.getElementById('userBtn'),uDd=document.getElementById('userDd');
|
||||
uBtn?.addEventListener('click',e=>{e.stopPropagation();uDd?.classList.toggle('open')});
|
||||
document.addEventListener('click',()=>uDd?.classList.remove('open'));
|
||||
|
||||
// Flash auto-dismiss after 4.5s
|
||||
setTimeout(()=>{
|
||||
document.querySelectorAll('.flash').forEach(el=>{
|
||||
el.style.transition='opacity .5s';
|
||||
el.style.opacity='0';
|
||||
setTimeout(()=>el.remove(),500);
|
||||
});
|
||||
},4500);
|
||||
|
||||
// Confirm dialogs
|
||||
document.querySelectorAll('[data-confirm]').forEach(el=>{
|
||||
el.addEventListener('click',e=>{if(!confirm(el.dataset.confirm))e.preventDefault();});
|
||||
});
|
||||
|
||||
// Admin sidebar active link
|
||||
document.querySelectorAll('.asl').forEach(l=>{
|
||||
const href=l.getAttribute('href');
|
||||
if(href&&(location.pathname===href||location.pathname.startsWith(href.replace(/\/[^/]+$/,'/')+'/')&&href!='/admin/'))
|
||||
l.classList.add('on');
|
||||
if(location.pathname===href)l.classList.add('on');
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$activeNav='attr'; $pageTitle='Attractions & Activities — Keyser & Mineral County WV';
|
||||
$attrs=qall("SELECT * FROM attractions WHERE is_active=1 ORDER BY sort_order,name");
|
||||
$filterCat=gs('cat'); $cats=array_unique(array_column($attrs,'category'));
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner">
|
||||
<div class="eyebrow">EXPLORE THE REGION</div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin-bottom:.4rem">Attractions & Activities</h1>
|
||||
<p style="color:var(--text-m)">From Civil War sites to breathtaking river scenery — Keyser and Mineral County have something for every visitor.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bar"><div class="filter-bar-inner">
|
||||
<a href="/attractions.php" class="btn btn-xs<?=!$filterCat?' btn-primary':' btn-outline'?>">All</a>
|
||||
<?php foreach($cats as $c): ?><a href="/attractions.php?cat=<?=urlencode($c)?>" class="btn btn-xs<?=$c===$filterCat?' btn-primary':' btn-outline'?>"><?=e($c)?></a><?php endforeach; ?>
|
||||
</div></div>
|
||||
<div class="section">
|
||||
<div class="g3">
|
||||
<?php foreach($attrs as $a): ?>
|
||||
<?php if($filterCat && $a['category']!==$filterCat) continue; ?>
|
||||
<div class="attr-card">
|
||||
<div class="attr-cat"><?=e($a['category'])?></div>
|
||||
<div class="attr-name"><?=e($a['name'])?></div>
|
||||
<div class="attr-desc"><?=e($a['description'])?></div>
|
||||
<?php if($a['address']): ?><div class="attr-det"><span>📍</span><span><?=e($a['address'])?></span></div><?php endif; ?>
|
||||
<?php if($a['phone']): ?><div class="attr-det"><span>📞</span><a href="tel:<?=e($a['phone'])?>"><?=e($a['phone'])?></a></div><?php endif; ?>
|
||||
<?php if($a['hours']): ?><div class="attr-det"><span>🕐</span><span><?=e($a['hours'])?></span></div><?php endif; ?>
|
||||
<?php if($a['website']): ?><div class="attr-det"><span>🌐</span><a href="https://<?=e($a['website'])?>" target="_blank"><?=e($a['website'])?> ↗</a></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:3.5rem">
|
||||
<div class="eyebrow" style="margin-bottom:1rem">GETTING HERE</div>
|
||||
<h2 class="sec-title" style="margin-bottom:1.5rem">Plan Your Visit to Keyser</h2>
|
||||
<div class="g3">
|
||||
<?php foreach([['By Car 🚗','Keyser sits at US Route 220 and US Route 50. Just 3 hours from Washington D.C., 20 minutes from Cumberland MD (via I-68), and 4 hours from Pittsburgh PA. Free parking throughout downtown.'],['By Air ✈️','The Greater Cumberland Regional Airport (CBE) in nearby Wiley Ford, WV is minutes away. Washington Dulles (IAD), Baltimore-Washington (BWI), and Pittsburgh (PIT) airports are all within 3–4 hours.'],['By Train 🚂','Amtrak Capitol Limited stops in Cumberland, MD — just across the Potomac from Ridgeley, WV (10 miles from Keyser). CSX freight lines run along the North Branch. The Potomac Valley Transit Authority (PVTA) provides local bus service.']] as [$t,$d]): ?>
|
||||
<div class="ibox"><div class="ibox-title"><?=e($t)?></div><p style="font-size:.88rem;color:var(--text-m);line-height:1.75"><?=e($d)?></p></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
+568
@@ -0,0 +1,568 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$slug = gs('slug');
|
||||
|
||||
$b = qone("
|
||||
SELECT
|
||||
b.id, b.name, b.slug, b.subcategory, b.description,
|
||||
b.address, b.phone, b.email, b.website,
|
||||
COALESCE(b.hours,'{}') AS hours,
|
||||
b.lat, b.lng, b.is_active, b.is_featured, b.created_at,
|
||||
b.category_id,
|
||||
COALESCE(c.name,'Uncategorized') AS cat,
|
||||
COALESCE(c.icon,'🏢') AS cat_icon,
|
||||
COALESCE(AVG(CASE WHEN r.is_approved=1 THEN r.rating END), 0) AS avg,
|
||||
COUNT(CASE WHEN r.is_approved=1 THEN r.id END) AS rcnt
|
||||
FROM businesses b
|
||||
LEFT JOIN categories c ON c.id = b.category_id
|
||||
LEFT JOIN reviews r ON r.business_id = b.id
|
||||
WHERE b.slug = ? AND b.is_active = 1
|
||||
GROUP BY b.id
|
||||
", [$slug]);
|
||||
|
||||
if (!$b) { flash('Business not found.','error'); go('/directory.php'); }
|
||||
|
||||
/* ── POST handlers ─────────────────────────────────────── */
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
// 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'];
|
||||
$submitted = 0;
|
||||
|
||||
// Simple text fields
|
||||
foreach ($allowedFields as $field) {
|
||||
$new = ps($field);
|
||||
$old = (string)($b[$field] ?? '');
|
||||
// For is_active, p() not ps()
|
||||
if ($field === 'is_active') {
|
||||
$new = (string)(int)p('is_active', $b['is_active']);
|
||||
$old = (string)(int)$b['is_active'];
|
||||
}
|
||||
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'",
|
||||
[$b['id'], $field]);
|
||||
qrun("INSERT INTO business_edit_requests(business_id,user_id,field,old_value,new_value)VALUES(?,?,?,?,?)",
|
||||
[$b['id'], uid(), $field, $old, $new]);
|
||||
$submitted++;
|
||||
}
|
||||
}
|
||||
|
||||
// Hours field — build JSON from individual day inputs
|
||||
$hrs = [];
|
||||
foreach ($days as $d) {
|
||||
$v = ps("hours_$d");
|
||||
if ($v !== '') $hrs[$d] = $v;
|
||||
}
|
||||
$newHours = json_encode($hrs);
|
||||
$oldHours = (string)($b['hours'] ?? '{}');
|
||||
if ($newHours !== $oldHours) {
|
||||
qrun("DELETE FROM business_edit_requests WHERE business_id=? AND field='hours' AND status='pending'",
|
||||
[$b['id']]);
|
||||
qrun("INSERT INTO business_edit_requests(business_id,user_id,field,old_value,new_value)VALUES(?,?,?,?,?)",
|
||||
[$b['id'], uid(), 'hours', $oldHours, $newHours]);
|
||||
$submitted++;
|
||||
}
|
||||
|
||||
if ($submitted > 0) {
|
||||
flash("$submitted change".($submitted>1?'s':'')." submitted for admin review.", 'success');
|
||||
} else {
|
||||
flash('No changes detected.', 'info');
|
||||
}
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'review' && authed()) {
|
||||
$rating = max(1, min(5, (int)p('rating', 5)));
|
||||
$title = ps('rtitle');
|
||||
$body = ps('rbody');
|
||||
$ex = qval("SELECT id FROM reviews WHERE business_id=? AND user_id=?", [$b['id'], uid()]);
|
||||
if ($ex) {
|
||||
qrun("UPDATE reviews SET rating=?,title=?,body=?,created_at=datetime('now') WHERE id=?",
|
||||
[$rating, $title, $body, $ex]);
|
||||
} else {
|
||||
qrun("INSERT INTO reviews(business_id,user_id,rating,title,body)VALUES(?,?,?,?,?)",
|
||||
[$b['id'], uid(), $rating, $title, $body]);
|
||||
}
|
||||
flash('Review saved!', 'success');
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'post_alert' && owns($b['id'])) {
|
||||
$t = ps('atitle');
|
||||
$body = ps('abody');
|
||||
$type = p('atype', 'info');
|
||||
if ($t && $body) {
|
||||
qrun("INSERT INTO alerts(business_id,type,title,body)VALUES(?,?,?,?)",
|
||||
[$b['id'], $type, $t, $body]);
|
||||
flash('Alert posted!', 'success');
|
||||
}
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'del_alert' && owns($b['id'])) {
|
||||
qrun("DELETE FROM alerts WHERE id=? AND business_id=?", [(int)p('aid'), $b['id']]);
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'del_review' && isAdmin()) {
|
||||
qrun("DELETE FROM reviews WHERE id=?", [(int)p('rid')]);
|
||||
flash('Review deleted.', 'success');
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
|
||||
if ($act === 'report' && authed()) {
|
||||
$rtype = p('rtype', 'correction');
|
||||
$rmsg = ps('rmsg');
|
||||
if ($rmsg) {
|
||||
qrun("INSERT INTO reports(business_id,user_id,type,message)VALUES(?,?,?,?)",
|
||||
[$b['id'], uid(), $rtype, $rmsg]);
|
||||
flash('Report submitted. Thank you!', 'success');
|
||||
}
|
||||
go("/business.php?slug=$slug");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Page data ─────────────────────────────────────────── */
|
||||
$pageTitle = e($b['name']).' — Keyser, WV';
|
||||
$activeNav = 'dir';
|
||||
|
||||
$reviews = qall("
|
||||
SELECT r.*, u.username
|
||||
FROM reviews r JOIN users u ON u.id = r.user_id
|
||||
WHERE r.business_id = ? AND r.is_approved = 1
|
||||
ORDER BY r.created_at DESC
|
||||
", [$b['id']]);
|
||||
|
||||
$sects = qall("SELECT * FROM menu_sections WHERE business_id=? ORDER BY sort_order", [$b['id']]);
|
||||
$mitems = [];
|
||||
foreach ($sects as $s) {
|
||||
$mitems[$s['id']] = qall(
|
||||
"SELECT * FROM menu_items WHERE section_id=? AND is_available=1 ORDER BY sort_order",
|
||||
[$s['id']]
|
||||
);
|
||||
}
|
||||
|
||||
$alerts = qall("SELECT * FROM alerts WHERE business_id=? AND is_active=1 ORDER BY created_at DESC", [$b['id']]);
|
||||
$myrev = authed() ? qone("SELECT * FROM reviews WHERE business_id=? AND user_id=?", [$b['id'], uid()]) : null;
|
||||
$hours = parseHours($b['hours']);
|
||||
$isOwner = owns($b['id']);
|
||||
$isNonAdminOwner = $isOwner && !isAdmin();
|
||||
$cat = (string)($b['cat'] ?? 'Uncategorized');
|
||||
$catIcon = (string)($b['cat_icon'] ?? '🏢');
|
||||
$days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
|
||||
|
||||
// Pending edit requests for this business (shown to owner)
|
||||
$pendingEdits = ($isOwner && !isAdmin())
|
||||
? qall("SELECT * FROM business_edit_requests WHERE business_id=? AND user_id=? AND status='pending' ORDER BY created_at DESC",
|
||||
[$b['id'], uid()])
|
||||
: [];
|
||||
$pendingByField = [];
|
||||
foreach ($pendingEdits as $pe) $pendingByField[$pe['field']] = $pe;
|
||||
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="detail-hdr">
|
||||
<div class="detail-hdr-inner">
|
||||
<div class="breadcrumb">
|
||||
<a href="/index.php">Home</a><span>›</span>
|
||||
<a href="/directory.php">Directory</a><span>›</span>
|
||||
<a href="/directory.php?cat=<?=urlencode($cat)?>"><?=e($catIcon.' '.$cat)?></a><span>›</span>
|
||||
<?=e($b['name'])?>
|
||||
</div>
|
||||
<div class="cat-badge"><?=e($catIcon.' '.$cat)?><?= $b['subcategory'] ? ' · '.e($b['subcategory']) : '' ?></div>
|
||||
<h1 style="font-size:clamp(1.9rem,4vw,2.9rem);margin:.4rem 0"><?=e($b['name'])?></h1>
|
||||
<div class="flex-row" style="margin-top:.6rem;gap:.9rem">
|
||||
<?=starDisplay((float)($b['avg'] ?? 0))?>
|
||||
<span class="rat-score"><?=number_format((float)($b['avg'] ?? 0), 1)?></span>
|
||||
<span class="rat-cnt">(<?=(int)($b['rcnt'] ?? 0)?> review<?=(int)($b['rcnt'] ?? 0) != 1 ? 's' : ''?>)</span>
|
||||
<?php if ($b['is_featured']): ?><span class="featured-badge">★ FEATURED</span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-body">
|
||||
<div><!-- main col -->
|
||||
|
||||
<?php if ($alerts): ?>
|
||||
<div style="margin-bottom:1.75rem">
|
||||
<div class="eyebrow" style="margin-bottom:.6rem">📢 BUSINESS UPDATES</div>
|
||||
<?php foreach ($alerts as $a): ?>
|
||||
<div class="alert-item a-<?=e($a['type'] ?? 'info')?>">
|
||||
<div class="alert-ico"><?=alertIcon($a['type'] ?? '')?></div>
|
||||
<div style="flex:1">
|
||||
<div class="alert-title"><?=e($a['title'] ?? '')?></div>
|
||||
<div class="alert-body"><?=e($a['body'] ?? '')?></div>
|
||||
<div class="alert-meta"><?=ago($a['created_at'] ?? '')?></div>
|
||||
</div>
|
||||
<?php if ($isOwner): ?>
|
||||
<form method="POST" style="margin-left:.5rem">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="del_alert">
|
||||
<input type="hidden" name="aid" value="<?=(int)$a['id']?>">
|
||||
<button class="btn btn-xs btn-danger" data-confirm="Delete this alert?">✕</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isOwner): ?>
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:1.75rem">
|
||||
<div class="ibox-title">📢 POST BUSINESS ALERT</div>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="post_alert">
|
||||
<div class="form-row" style="margin-bottom:.7rem">
|
||||
<input type="text" name="atitle" class="finput" placeholder="Alert title" required>
|
||||
<select name="atype" class="fselect">
|
||||
<option value="info">ℹ️ Info</option>
|
||||
<option value="news">📰 News</option>
|
||||
<option value="event">🎉 Event</option>
|
||||
<option value="warning">⚠️ Warning</option>
|
||||
<option value="sale">🏷️ Sale / Promo</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea name="abody" class="ftextarea" style="min-height:65px" placeholder="Alert details…" required></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top:.65rem">Post Alert</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ── OWNER EDIT PANEL (non-admin owners only) ───────── -->
|
||||
<?php if ($isNonAdminOwner): ?>
|
||||
<div class="ibox" style="border:2px solid var(--navy-m);margin-bottom:1.75rem">
|
||||
<div class="ibox-title" style="color:var(--text)">
|
||||
✏️ SUGGEST LISTING CHANGES
|
||||
<span style="font-family:'Source Sans 3',sans-serif;font-weight:400;font-size:.78rem;color:var(--text-m);letter-spacing:0;margin-left:.5rem">
|
||||
— Changes require admin approval before going live
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if ($pendingEdits): ?>
|
||||
<div style="background:rgba(201,168,76,.08);border:1px solid var(--gold-dim);border-radius:4px;padding:.85rem 1rem;margin-bottom:1.1rem;font-size:.84rem">
|
||||
<strong style="color:var(--gold)">⏳ Pending review:</strong>
|
||||
<span style="color:var(--text-m)">
|
||||
<?= implode(', ', array_map(fn($pe) => '<strong>'.e(ucfirst($pe['field'])).'</strong>', $pendingEdits)) ?>
|
||||
</span>
|
||||
— an admin will review these changes shortly.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="owner_edit">
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">
|
||||
DESCRIPTION
|
||||
<?php if (isset($pendingByField['description'])): ?>
|
||||
<span class="badge badge-gold" style="margin-left:.4rem">PENDING</span>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
<textarea name="description" class="ftextarea" style="min-height:90px"><?=e(isset($pendingByField['description']) ? $pendingByField['description']['new_value'] : $b['description'])?></textarea>
|
||||
<?php if (isset($pendingByField['description'])): ?>
|
||||
<div class="fhint" style="color:var(--amber)">Currently pending: "<?=e(substr($pendingByField['description']['new_value'],0,80))?><?= strlen($pendingByField['description']['new_value'])>80?'…':'' ?>"</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">
|
||||
ADDRESS
|
||||
<?php if (isset($pendingByField['address'])): ?><span class="badge badge-gold" style="margin-left:.4rem">PENDING</span><?php endif; ?>
|
||||
</label>
|
||||
<input type="text" name="address" class="finput"
|
||||
value="<?=e(isset($pendingByField['address']) ? $pendingByField['address']['new_value'] : $b['address'])?>">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">
|
||||
PHONE
|
||||
<?php if (isset($pendingByField['phone'])): ?><span class="badge badge-gold" style="margin-left:.4rem">PENDING</span><?php endif; ?>
|
||||
</label>
|
||||
<input type="text" name="phone" class="finput"
|
||||
value="<?=e(isset($pendingByField['phone']) ? $pendingByField['phone']['new_value'] : $b['phone'])?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">
|
||||
EMAIL
|
||||
<?php if (isset($pendingByField['email'])): ?><span class="badge badge-gold" style="margin-left:.4rem">PENDING</span><?php endif; ?>
|
||||
</label>
|
||||
<input type="email" name="email" class="finput"
|
||||
value="<?=e(isset($pendingByField['email']) ? $pendingByField['email']['new_value'] : $b['email'])?>">
|
||||
</div>
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">
|
||||
HOURS OF OPERATION
|
||||
<?php if (isset($pendingByField['hours'])): ?><span class="badge badge-gold" style="margin-left:.4rem">PENDING</span><?php endif; ?>
|
||||
</label>
|
||||
<?php
|
||||
// Show pending hours if exists, else current
|
||||
$editHours = isset($pendingByField['hours'])
|
||||
? (parseHours($pendingByField['hours']['new_value']) ?: $hours)
|
||||
: $hours;
|
||||
?>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:.45rem;margin-top:.4rem">
|
||||
<?php foreach ($days as $d): ?>
|
||||
<div style="display:flex;gap:.4rem;align-items:center">
|
||||
<span style="font-family:'Oswald',sans-serif;font-size:.72rem;color:var(--gold);width:30px;flex-shrink:0"><?=$d?></span>
|
||||
<input type="text" name="hours_<?=$d?>" class="finput" style="font-size:.8rem;padding:.38rem .6rem"
|
||||
value="<?=e($editHours[$d] ?? '')?>" placeholder="e.g. 9am–5pm or Closed">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="fhint">Leave blank to omit a day from the hours display.</div>
|
||||
</div>
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">
|
||||
LISTING STATUS
|
||||
<?php if (isset($pendingByField['is_active'])): ?><span class="badge badge-gold" style="margin-left:.4rem">PENDING</span><?php endif; ?>
|
||||
</label>
|
||||
<?php $currentActive = isset($pendingByField['is_active']) ? (int)$pendingByField['is_active']['new_value'] : (int)$b['is_active']; ?>
|
||||
<div class="flex-row" style="gap:1.5rem;margin-top:.35rem">
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.9rem">
|
||||
<input type="radio" name="is_active" value="1"<?= $currentActive===1?' checked':''?>>
|
||||
<span style="color:var(--green-l)">✅ Open / Active</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.9rem">
|
||||
<input type="radio" name="is_active" value="0"<?= $currentActive===0?' checked':''?>>
|
||||
<span style="color:var(--text-d)">🔴 Temporarily Closed / Inactive</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="fhint">Setting to Inactive will hide this listing from the public directory until an admin approves and it goes live.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-navy">Submit Changes for Review</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- ── END OWNER EDIT PANEL ──────────────────────────── -->
|
||||
|
||||
<div class="ibox" style="margin-bottom:1.75rem">
|
||||
<div class="ibox-title">ABOUT <?=strtoupper(e($b['name']))?></div>
|
||||
<p style="color:var(--text-m);line-height:1.85"><?=e($b['description'] ?? '')?></p>
|
||||
</div>
|
||||
|
||||
<?php if ($sects): ?>
|
||||
<div class="ibox" style="margin-bottom:1.75rem">
|
||||
<div class="ibox-title">🍽️ MENU</div>
|
||||
<?php foreach ($sects as $s): ?>
|
||||
<div class="menu-sec">
|
||||
<div class="menu-sec-name"><?=e($s['name'] ?? '')?></div>
|
||||
<?php if (!empty($s['description'])): ?>
|
||||
<div class="menu-sec-desc"><?=e($s['description'])?></div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($mitems[$s['id']] as $mi): ?>
|
||||
<div class="menu-item">
|
||||
<div class="mi-info">
|
||||
<div class="mi-name"><?=e($mi['name'] ?? '')?></div>
|
||||
<?php if (!empty($mi['description'])): ?>
|
||||
<div class="mi-desc"><?=e($mi['description'])?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ((float)($mi['price'] ?? 0) > 0): ?>
|
||||
<div class="mi-price"><?=money((float)$mi['price'])?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($isOwner): ?>
|
||||
<a href="/menu.php?id=<?=(int)$b['id']?>" class="btn btn-outline btn-sm">✏️ Manage Menu</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif ($isOwner): ?>
|
||||
<div style="margin-bottom:1.75rem">
|
||||
<a href="/menu.php?id=<?=(int)$b['id']?>" class="btn btn-outline">+ Add Menu</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3 style="font-family:'Oswald',sans-serif;letter-spacing:.1em;font-size:1.05rem;color:var(--text);margin-bottom:1.1rem;padding-bottom:.6rem;border-bottom:1px solid var(--bdr)">
|
||||
⭐ REVIEWS (<?=(int)($b['rcnt'] ?? 0)?>)
|
||||
</h3>
|
||||
|
||||
<?php if (authed()): ?>
|
||||
<div class="ibox" style="margin-bottom:1.5rem">
|
||||
<div class="ibox-title"><?= $myrev ? 'UPDATE YOUR REVIEW' : 'WRITE A REVIEW' ?></div>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="review">
|
||||
<div class="fg">
|
||||
<label class="flabel">YOUR RATING</label>
|
||||
<?=starPicker('rating', (int)($myrev['rating'] ?? 0))?>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">TITLE (optional)</label>
|
||||
<input type="text" name="rtitle" class="finput"
|
||||
value="<?=e($myrev['title'] ?? '')?>" placeholder="One-line summary…">
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">YOUR REVIEW</label>
|
||||
<textarea name="rbody" class="ftextarea" placeholder="Share your experience…"><?=e($myrev['body'] ?? '')?></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Submit Review</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="background:var(--bg4);border:1px solid var(--bdr);border-radius:6px;padding:1.1rem;text-align:center;margin-bottom:1.4rem">
|
||||
<a href="/login.php" class="btn btn-outline btn-sm">Login to Write a Review</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($reviews as $r): ?>
|
||||
<div class="rev-card">
|
||||
<div class="rev-hdr">
|
||||
<div>
|
||||
<div class="rev-author"><?=e($r['username'] ?? 'Anonymous')?></div>
|
||||
<?=starDisplay((float)($r['rating'] ?? 0))?>
|
||||
</div>
|
||||
<div class="flex-row" style="gap:.5rem">
|
||||
<span class="rev-date"><?=ago($r['created_at'] ?? '')?></span>
|
||||
<?php if (isAdmin()): ?>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="del_review">
|
||||
<input type="hidden" name="rid" value="<?=(int)$r['id']?>">
|
||||
<button class="btn btn-xs btn-danger" data-confirm="Delete this review?">Delete</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($r['title'])): ?><div class="rev-title"><?=e($r['title'])?></div><?php endif; ?>
|
||||
<?php if (!empty($r['body'])): ?><div class="rev-body"><?=e($r['body'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!$reviews): ?>
|
||||
<p style="color:var(--text-d);font-size:.88rem">No reviews yet — be the first!</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<hr class="divider">
|
||||
<?php if (authed()): ?>
|
||||
<details>
|
||||
<summary style="cursor:pointer;font-size:.82rem;color:var(--text-d);font-family:'Oswald',sans-serif;letter-spacing:.06em">
|
||||
🚩 REPORT A LISTING ISSUE
|
||||
</summary>
|
||||
<div class="ibox" style="margin-top:.75rem">
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<input type="hidden" name="_act" value="report">
|
||||
<div class="fg">
|
||||
<label class="flabel">ISSUE TYPE</label>
|
||||
<select name="rtype" class="fselect">
|
||||
<option value="correction">Incorrect Information</option>
|
||||
<option value="closed">Business Closed / Moved</option>
|
||||
<option value="hours">Wrong Hours</option>
|
||||
<option value="phone">Wrong Phone Number</option>
|
||||
<option value="other">Other Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">DESCRIBE THE ISSUE</label>
|
||||
<textarea name="rmsg" class="ftextarea" style="min-height:75px" required
|
||||
placeholder="Please describe what needs correction…"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Submit Report</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<?php else: ?>
|
||||
<p style="font-size:.82rem;color:var(--text-d)">
|
||||
🚩 <a href="/login.php">Log in</a> to report a listing issue.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /main col -->
|
||||
|
||||
<div><!-- sidebar -->
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">BUSINESS INFO</div>
|
||||
<?php if (!empty($b['address'])): ?>
|
||||
<div class="irow"><span class="irow-i">📍</span><span class="irow-v"><?=e($b['address'])?></span></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($b['phone'])): ?>
|
||||
<div class="irow"><span class="irow-i">📞</span>
|
||||
<a href="tel:<?=e($b['phone'])?>" class="irow-v" style="color:var(--gold)"><?=e($b['phone'])?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($b['email'])): ?>
|
||||
<div class="irow"><span class="irow-i">✉️</span>
|
||||
<a href="mailto:<?=e($b['email'])?>" class="irow-v" style="color:var(--gold)"><?=e($b['email'])?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($b['website'])): ?>
|
||||
<div class="irow"><span class="irow-i">🌐</span>
|
||||
<a href="https://<?=e($b['website'])?>" target="_blank" rel="noopener"
|
||||
class="irow-v" style="color:var(--gold);word-break:break-all"><?=e($b['website'])?> ↗</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="irow">
|
||||
<span class="irow-i">🏷️</span>
|
||||
<span class="irow-v"><?=e($cat)?><?= !empty($b['subcategory']) ? ' · '.e($b['subcategory']) : '' ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($hours): ?>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">🕐 HOURS OF OPERATION</div>
|
||||
<div class="hrs-grid">
|
||||
<?php foreach ($hours as $day => $time): ?>
|
||||
<div class="hrs-row">
|
||||
<span class="hrs-day"><?=strtoupper(substr(e((string)$day), 0, 3))?></span>
|
||||
<span class="hrs-time"><?=e((string)$time)?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($b['address'])): ?>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title">📍 LOCATION</div>
|
||||
<div style="background:var(--bg4);border:1px dashed var(--bdr);border-radius:4px;padding:1.75rem;text-align:center">
|
||||
<p style="font-size:.84rem;color:var(--text-m);margin-bottom:.75rem"><?=e($b['address'])?></p>
|
||||
<a href="https://maps.google.com/?q=<?=urlencode($b['name'].' '.$b['address'])?>"
|
||||
target="_blank" rel="noopener" class="btn btn-outline btn-sm">Open in Google Maps ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isOwner): ?>
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">⚙️ OWNER CONTROLS</div>
|
||||
<a href="/menu.php?id=<?=(int)$b['id']?>" class="btn btn-outline btn-sm btn-block"
|
||||
style="margin-bottom:.5rem">🍽️ Manage Menu</a>
|
||||
<?php if (isAdmin()): ?>
|
||||
<a href="/admin/biz_edit.php?id=<?=(int)$b['id']?>" class="btn btn-secondary btn-sm btn-block"
|
||||
style="margin-bottom:.5rem">✏️ Edit Listing (Admin)</a>
|
||||
<a href="/admin/biz_edits.php?biz_id=<?=(int)$b['id']?>" class="btn btn-secondary btn-sm btn-block">
|
||||
📋 Review Owner Edits
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$pendingCount = (int)qval(
|
||||
"SELECT COUNT(*) FROM business_edit_requests WHERE business_id=? AND status='pending'",
|
||||
[$b['id']]
|
||||
);
|
||||
?>
|
||||
<?php if ($pendingCount): ?>
|
||||
<div style="text-align:center;font-size:.82rem;color:var(--amber);margin-top:.25rem">
|
||||
⏳ <?=$pendingCount?> change<?=$pendingCount>1?'s':''?> awaiting admin review
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /sidebar -->
|
||||
</div><!-- /detail-body -->
|
||||
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
requireLogin();
|
||||
$pageTitle = 'My Dashboard — Keyser WV';
|
||||
$bizIds = isAdmin()
|
||||
? array_column(qall("SELECT id FROM businesses"),'id')
|
||||
: array_column(qall("SELECT business_id FROM business_owners WHERE user_id=?",[uid()]),'business_id');
|
||||
$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()]) : [];
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner">
|
||||
<div class="eyebrow">MY ACCOUNT</div>
|
||||
<h1 style="font-size:2rem;margin-bottom:.3rem">Dashboard — <?=e(uname())?></h1>
|
||||
<p style="color:var(--text-m)">Manage your listings, menus, alerts, and submitted events.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<?php if($myBiz): ?>
|
||||
<div class="eyebrow" style="margin-bottom:1rem">MY BUSINESS LISTINGS</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1.4rem;margin-bottom:2.5rem">
|
||||
<?php foreach($myBiz as $b): ?>
|
||||
<div class="ibox">
|
||||
<div class="ibox-title"><?=e($b['name'])?></div>
|
||||
<div style="font-size:.82rem;color:var(--text-m);margin-bottom:.75rem"><?=e($b['cat'])?><?=$b['subcategory']?' · '.e($b['subcategory']):''?></div>
|
||||
<div class="flex-row" style="gap:.4rem;margin-bottom:.85rem"><?=starDisplay((float)$b['avg'])?><span class="rat-score"><?=number_format((float)$b['avg'],1)?></span><span class="rat-cnt">(<?=$b['rcnt']?> reviews)</span></div>
|
||||
<?php if($b['address']): ?><div style="font-size:.8rem;color:var(--text-d);margin-bottom:.75rem">📍 <?=e($b['address'])?></div><?php endif; ?>
|
||||
<div class="flex-row" style="gap:.5rem;flex-wrap:wrap">
|
||||
<a href="/business.php?slug=<?=e($b['slug'])?>" class="btn btn-outline btn-xs">View Listing</a>
|
||||
<a href="/menu.php?id=<?=$b['id']?>" class="btn btn-secondary btn-xs">🍽️ Menu</a>
|
||||
<?php if(isAdmin()): ?><a href="/admin/businesses.php?edit=<?=$b['id']?>" class="btn btn-secondary btn-xs">✏️ Edit</a><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty" style="padding:2rem"><div class="empty-ico">🏢</div><h3>No businesses assigned yet</h3><p style="margin-top:.5rem">Contact an administrator to link your business listing.</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($myEvents): ?>
|
||||
<div class="eyebrow" style="margin-bottom:.8rem">MY SUBMITTED EVENTS</div>
|
||||
<div class="tbl-wrap" style="margin-bottom:2.5rem">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>EVENT</th><th>DATE</th><th>STATUS</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($myEvents as $ev): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($ev['title'])?></td>
|
||||
<td><?=fdate($ev['event_date'])?></td>
|
||||
<td><?php $sc=['approved'=>'badge-green','pending'=>'badge-gold','rejected'=>'badge-red']; ?>
|
||||
<span class="badge <?=$sc[$ev['status']]??'badge-gray'?>"><?=strtoupper($ev['status'])?></span></td>
|
||||
<td><?php if($ev['status']==='approved'): ?><a href="/events.php" class="btn btn-xs btn-outline">View</a><?php endif; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="eyebrow" style="margin-bottom:1rem">QUICK ACTIONS</div>
|
||||
<div class="flex-row">
|
||||
<a href="/events.php?submit=1" class="btn btn-outline">📅 Submit Event</a>
|
||||
<a href="/account.php" class="btn btn-secondary">⚙️ Account Settings</a>
|
||||
<?php if(isAdmin()): ?><a href="/admin/" class="btn btn-navy">⚙ Admin Panel</a><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,3 @@
|
||||
<FilesMatch "\.db$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$activeNav='dir';
|
||||
$q = gs('q');
|
||||
$cat = gs('cat');
|
||||
$page = max(1, iget('page'));
|
||||
$perPage = 18;
|
||||
|
||||
$where = "WHERE b.is_active=1";
|
||||
$params = [];
|
||||
if ($q) {
|
||||
$where .= " AND (b.name LIKE ? OR b.description LIKE ? OR b.subcategory LIKE ? OR b.address LIKE ?)";
|
||||
$lk = "%$q%";
|
||||
$params = [$lk, $lk, $lk, $lk];
|
||||
}
|
||||
if ($cat) {
|
||||
$where .= " AND c.name=?";
|
||||
$params[] = $cat;
|
||||
}
|
||||
|
||||
$total = (int)qval("SELECT COUNT(*) FROM businesses b LEFT JOIN categories c ON c.id=b.category_id $where", $params);
|
||||
$pages = max(1, (int)ceil($total / $perPage));
|
||||
$page = min($page, $pages);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$bizs = qall("
|
||||
SELECT b.*,
|
||||
COALESCE(c.name,'Uncategorized') AS cat,
|
||||
COALESCE(c.icon,'🏢') AS cat_icon,
|
||||
COALESCE(AVG(r.rating),0) AS avg,
|
||||
COUNT(r.id) AS rcnt
|
||||
FROM businesses b
|
||||
LEFT 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
|
||||
GROUP BY b.id
|
||||
ORDER BY b.is_featured DESC, b.name ASC
|
||||
LIMIT $perPage OFFSET $offset
|
||||
", $params);
|
||||
|
||||
$cats = qall("SELECT DISTINCT c.name, c.icon FROM categories c
|
||||
JOIN businesses b ON b.category_id = c.id
|
||||
WHERE b.is_active = 1
|
||||
ORDER BY c.sort_order");
|
||||
|
||||
$pageTitle = 'Business Directory — Keyser, WV';
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner">
|
||||
<div class="eyebrow">EXPLORE KEYSER</div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin-bottom:.4rem">Business Directory</h1>
|
||||
<p style="color:var(--text-m)">Discover <?=$total?> local businesses, restaurants, services, and more in Keyser, West Virginia.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<div class="search-bar-inner">
|
||||
<form method="GET" class="search-form">
|
||||
<div class="search-wrap">
|
||||
<span class="s-icon">🔍</span>
|
||||
<input class="search-input" type="text" name="q" value="<?=e($q)?>" placeholder="Search businesses…">
|
||||
</div>
|
||||
<select name="cat" class="finput" style="flex:0 0 auto;width:auto;padding:.72rem 1rem">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach ($cats as $c): ?>
|
||||
<option value="<?=e($c['name'])?>"<?= $c['name']===$cat ? ' selected' : '' ?>><?=e($c['icon'].' '.$c['name'])?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
<?php if ($q || $cat): ?><a href="/directory.php" class="btn btn-secondary">Clear</a><?php endif; ?>
|
||||
</form>
|
||||
<?php if ($q || $cat): ?>
|
||||
<p style="margin-top:.65rem;font-size:.84rem;color:var(--text-m)">
|
||||
Found <strong style="color:var(--gold)"><?=$total?></strong> result<?=$total!==1?'s':''?>
|
||||
<?php if ($q): ?> for "<em><?=e($q)?></em>"<?php endif; ?>
|
||||
<?php if ($cat): ?> in <em><?=e($cat)?></em><?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-bar-inner">
|
||||
<a href="/directory.php<?= $q ? '?q='.urlencode($q) : '' ?>" class="btn btn-xs<?= !$cat ? ' btn-primary' : ' btn-outline' ?>">All</a>
|
||||
<?php foreach ($cats as $c): ?>
|
||||
<a href="/directory.php?cat=<?=urlencode($c['name'])?><?= $q ? '&q='.urlencode($q) : '' ?>"
|
||||
class="btn btn-xs<?= $c['name']===$cat ? ' btn-primary' : ' btn-outline' ?>"><?=e($c['icon'].' '.$c['name'])?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<?php if ($bizs): ?>
|
||||
<div class="g3">
|
||||
<?php foreach ($bizs as $b): ?>
|
||||
<a href="/business.php?slug=<?=e($b['slug'])?>" style="text-decoration:none">
|
||||
<div class="biz-card">
|
||||
<div class="biz-card-top">
|
||||
<div class="cat-badge"><?=e($b['cat_icon'].' '.$b['cat'])?></div>
|
||||
<?php if ($b['is_featured']): ?><span class="featured-badge" style="float:right">★</span><?php endif; ?>
|
||||
<div class="biz-name"><?=e($b['name'])?></div>
|
||||
<?php if ($b['subcategory']): ?><div class="biz-sub"><?=e($b['subcategory'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="biz-card-body">
|
||||
<div class="biz-desc"><?=e($b['description'])?></div>
|
||||
<div class="biz-meta">
|
||||
<?php if ($b['address']): ?>
|
||||
<div class="biz-meta-row"><span class="biz-meta-icon">📍</span><?=e($b['address'])?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($b['phone']): ?>
|
||||
<div class="biz-meta-row"><span class="biz-meta-icon">📞</span><?=e($b['phone'])?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="biz-card-foot">
|
||||
<div class="flex-row" style="gap:.4rem">
|
||||
<?=starDisplay((float)$b['avg'])?>
|
||||
<span class="rat-score"><?=number_format((float)$b['avg'],1)?></span>
|
||||
<span class="rat-cnt">(<?=$b['rcnt']?> review<?=$b['rcnt']!=1?'s':''?>)</span>
|
||||
</div>
|
||||
<span style="color:var(--gold);font-size:.76rem;font-family:'Oswald',sans-serif">VIEW →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($pages > 1): ?>
|
||||
<div style="display:flex;justify-content:center;gap:.5rem;margin-top:2.5rem;flex-wrap:wrap">
|
||||
<?php for ($i = 1; $i <= $pages; $i++): ?>
|
||||
<a href="?<?=http_build_query(array_filter(['q'=>$q,'cat'=>$cat,'page'=>$i]))?>"
|
||||
class="btn btn-sm<?= $i===$page ? ' btn-primary' : ' btn-outline' ?>"><?=$i?></a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="empty">
|
||||
<div class="empty-ico">🔍</div>
|
||||
<h3>No results found</h3>
|
||||
<p style="margin-top:.5rem">Try different search terms or browse all categories.</p>
|
||||
<a href="/directory.php" class="btn btn-outline" style="margin-top:1.5rem">Browse All</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$activeNav='events'; $pageTitle='Events — Keyser, WV';
|
||||
$showForm=!empty($_GET['submit']);
|
||||
$errors=[];
|
||||
|
||||
if(isPost()&&ps('_act')==='submit_event'){
|
||||
csrfCheck();
|
||||
if(!authed()){flash('Please log in to submit an event.','warning');go('/login.php?next=/events.php?submit=1');}
|
||||
$title=ps('title');$desc=ps('description');$venue=ps('venue');$addr=ps('address');
|
||||
$date=ps('event_date');$time=ps('event_time');$edate=ps('end_date');$etime=ps('end_time');
|
||||
$cost=ps('cost');$web=ps('website');$cn=ps('contact_name');$ce=ps('contact_email');$cp=ps('contact_phone');
|
||||
if(!$title) $errors[]='Event title is required.';
|
||||
if(!$date) $errors[]='Event date is required.';
|
||||
if(!$errors){
|
||||
$status=setting('events_require_approval','1')==='1'?'pending':'approved';
|
||||
qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,submitted_by,status)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
[$title,$desc,$venue,$addr,$date,$time,$edate?:null,$etime,$cost,$web,$cn,$ce,$cp,uid(),$status]);
|
||||
flash($status==='approved'?'Event submitted and published!':'Event submitted! It will appear after review.','success');
|
||||
go('/events.php');
|
||||
}
|
||||
$showForm=true;
|
||||
}
|
||||
|
||||
$page=max(1,iget('page')); $perPage=10;
|
||||
$total=(int)qval("SELECT COUNT(*) FROM events WHERE status='approved' AND(event_date>=date('now') OR(end_date IS NOT NULL AND end_date>=date('now')))");
|
||||
$pages=max(1,(int)ceil($total/$perPage)); $page=min($page,$pages); $offset=($page-1)*$perPage;
|
||||
$events=qall("SELECT e.*,u.username submitter FROM events e LEFT JOIN users u ON u.id=e.submitted_by WHERE e.status='approved' AND(e.event_date>=date('now') OR(e.end_date IS NOT NULL AND e.end_date>=date('now'))) ORDER BY e.is_featured DESC,e.event_date ASC LIMIT $perPage OFFSET $offset");
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner" style="display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:1rem">
|
||||
<div>
|
||||
<div class="eyebrow">COMMUNITY CALENDAR</div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin-bottom:.4rem">Events in Keyser, WV</h1>
|
||||
<p style="color:var(--text-m)">Festivals, sports, community gatherings, arts, and more in Mineral County.</p>
|
||||
</div>
|
||||
<?php if(authed()): ?>
|
||||
<a href="/events.php?submit=1" class="btn btn-primary">+ Submit Event</a>
|
||||
<?php else: ?>
|
||||
<a href="/login.php?next=/events.php?submit=1" class="btn btn-outline">Login to Submit Event</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($showForm&&authed()): ?>
|
||||
<div class="section-sm" style="padding-bottom:0">
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">📅 SUBMIT A COMMUNITY EVENT</div>
|
||||
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="submit_event">
|
||||
<div class="fg"><label class="flabel">EVENT TITLE *</label><input type="text" name="title" class="finput" value="<?=e($_POST['title']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><textarea name="description" class="ftextarea" style="min-height:110px"><?=e($_POST['description']??'')?></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">VENUE / LOCATION</label><input type="text" name="venue" class="finput" value="<?=e($_POST['venue']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">ADDRESS</label><input type="text" name="address" class="finput" value="<?=e($_POST['address']??'')?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">START DATE *</label><input type="date" name="event_date" class="finput" value="<?=e($_POST['event_date']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">START TIME</label><input type="text" name="event_time" class="finput" value="<?=e($_POST['event_time']??'')?>" placeholder="e.g. 7:00 PM"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">END DATE (if multi-day)</label><input type="date" name="end_date" class="finput" value="<?=e($_POST['end_date']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">END TIME</label><input type="text" name="end_time" class="finput" value="<?=e($_POST['end_time']??'')?>" placeholder="e.g. 10:00 PM"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">COST / ADMISSION</label><input type="text" name="cost" class="finput" value="<?=e($_POST['cost']??'Free')?>" placeholder="Free, $10, Varies…"></div>
|
||||
<div class="fg"><label class="flabel">WEBSITE</label><input type="text" name="website" class="finput" value="<?=e($_POST['website']??'')?>" placeholder="example.com"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">CONTACT NAME</label><input type="text" name="contact_name" class="finput" value="<?=e($_POST['contact_name']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">CONTACT PHONE</label><input type="text" name="contact_phone" class="finput" value="<?=e($_POST['contact_phone']??'')?>"></div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button type="submit" class="btn btn-primary">Submit Event</button>
|
||||
<a href="/events.php" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
<?php if(setting('events_require_approval','1')==='1'): ?><p class="fhint" style="margin-top:.65rem">Events are reviewed by an administrator before appearing publicly.</p><?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif(!empty($_GET['submit'])&&!authed()): ?>
|
||||
<div style="text-align:center;padding:3rem;color:var(--text-m)"><p>Please <a href="/login.php?next=/events.php?submit=1">log in</a> to submit an event.</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="section">
|
||||
<?php if($events): ?>
|
||||
<?php foreach($events as $ev): ?>
|
||||
<div class="evt-card">
|
||||
<div class="evt-date-box">
|
||||
<div class="evt-mon"><?=strtoupper(date('M',strtotime($ev['event_date'])))?></div>
|
||||
<div class="evt-day"><?=date('j',strtotime($ev['event_date']))?></div>
|
||||
<div class="evt-yr"><?=date('Y',strtotime($ev['event_date']))?></div>
|
||||
</div>
|
||||
<div class="evt-body">
|
||||
<div class="evt-title"><?=e($ev['title'])?><?php if($ev['is_featured']): ?> <span class="featured-badge" style="margin-left:.5rem">★ FEATURED</span><?php endif; ?></div>
|
||||
<div class="evt-meta">
|
||||
<?php if($ev['venue']): ?><span>📍 <?=e($ev['venue'])?></span><?php endif; ?>
|
||||
<?php if($ev['event_time']): ?><span>🕐 <?=e($ev['event_time'])?></span><?php endif; ?>
|
||||
<?php if($ev['end_date']&&$ev['end_date']!==$ev['event_date']): ?><span>📅 thru <?=fdate($ev['end_date'])?></span><?php endif; ?>
|
||||
<span>💰 <?=e($ev['cost'])?></span>
|
||||
<?php if($ev['contact_phone']): ?><span>📞 <?=e($ev['contact_phone'])?></span><?php endif; ?>
|
||||
</div>
|
||||
<div class="evt-desc"><?=e($ev['description'])?></div>
|
||||
<?php if($ev['website']): ?><div class="evt-actions"><a href="https://<?=e($ev['website'])?>" target="_blank" class="btn btn-outline btn-sm">More Info ↗</a></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if($pages>1): ?><div style="display:flex;justify-content:center;gap:.5rem;margin-top:2rem;flex-wrap:wrap"><?php for($i=1;$i<=$pages;$i++): ?><a href="?page=<?=$i?>" class="btn btn-sm<?=$i===$page?' btn-primary':' btn-outline'?>"><?=$i?></a><?php endfor; ?></div><?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="empty"><div class="empty-ico">📅</div><h3>No upcoming events listed</h3><p style="margin-top:.5rem">Be the first to submit a community event!</p><?php if(authed()): ?><a href="/events.php?submit=1" class="btn btn-primary" style="margin-top:1.5rem">Submit an Event</a><?php endif; ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,842 @@
|
||||
<?php
|
||||
/**
|
||||
* Keyser WV Tourism Site — Database Layer
|
||||
* SQLite via PDO; DB lives in /data/ (chmod 777)
|
||||
*/
|
||||
define('DATA_DIR', dirname(__DIR__).'/data');
|
||||
define('DB_FILE', DATA_DIR.'/keyser.db');
|
||||
|
||||
function db(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
if (!is_dir(DATA_DIR)) mkdir(DATA_DIR, 0777, true);
|
||||
@chmod(DATA_DIR, 0777);
|
||||
$isNew = !file_exists(DB_FILE) || filesize(DB_FILE) === 0;
|
||||
$pdo = new PDO('sqlite:'.DB_FILE, null, null, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$pdo->exec("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA synchronous=NORMAL;");
|
||||
if ($isNew) { _schema($pdo); _seed($pdo); }
|
||||
if (file_exists(DB_FILE)) @chmod(DB_FILE, 0666);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/* ── Shorthand query helpers ────────────────────────────── */
|
||||
function qr(string $sql, array $p=[]): PDOStatement { $s=db()->prepare($sql); $s->execute($p); return $s; }
|
||||
function qall(string $sql, array $p=[]): array { return qr($sql,$p)->fetchAll(); }
|
||||
function qone(string $sql, array $p=[]): ?array { $r=qr($sql,$p)->fetch(); return $r?:null; }
|
||||
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(); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
SCHEMA
|
||||
══════════════════════════════════════════════════════ */
|
||||
function _schema(PDO $db): void {
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS settings(
|
||||
key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT,
|
||||
password TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
last_login TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS categories(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🏢',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS businesses(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
category_id INTEGER REFERENCES categories(id),
|
||||
subcategory TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
website TEXT NOT NULL DEFAULT '',
|
||||
hours TEXT NOT NULL DEFAULT '{}',
|
||||
lat REAL NOT NULL DEFAULT 39.4364,
|
||||
lng REAL NOT NULL DEFAULT -78.9762,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
is_featured INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS business_owners(
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(user_id,business_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS reviews(
|
||||
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,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
is_approved INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS menu_sections(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS menu_items(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
section_id INTEGER NOT NULL REFERENCES menu_sections(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
price REAL NOT NULL DEFAULT 0,
|
||||
is_available INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS alerts(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL DEFAULT 'info',
|
||||
title TEXT NOT NULL,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS attractions(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'Sightseeing',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
website TEXT NOT NULL DEFAULT '',
|
||||
hours TEXT NOT NULL DEFAULT '',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS events(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
venue TEXT NOT NULL DEFAULT '',
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
event_date TEXT NOT NULL,
|
||||
event_time TEXT NOT NULL DEFAULT '',
|
||||
end_date TEXT,
|
||||
end_time TEXT NOT NULL DEFAULT '',
|
||||
cost TEXT NOT NULL DEFAULT 'Free',
|
||||
website TEXT NOT NULL DEFAULT '',
|
||||
contact_name TEXT NOT NULL DEFAULT '',
|
||||
contact_email TEXT NOT NULL DEFAULT '',
|
||||
contact_phone TEXT NOT NULL DEFAULT '',
|
||||
submitted_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
is_featured INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS reports(
|
||||
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,
|
||||
type TEXT NOT NULL DEFAULT 'correction',
|
||||
message TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
admin_notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
SEED DATA (real Keyser WV businesses from city website
|
||||
+ review sites + Wikipedia)
|
||||
══════════════════════════════════════════════════════ */
|
||||
function _seed(PDO $db): void {
|
||||
|
||||
/* Settings */
|
||||
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)
|
||||
qrun("INSERT OR IGNORE INTO settings(key,value)VALUES(?,?)",[$k,$v]);
|
||||
|
||||
/* Admin user — password "password123" */
|
||||
$hash = password_hash('password123', PASSWORD_BCRYPT);
|
||||
qrun("INSERT OR IGNORE INTO users(username,email,password,is_admin)
|
||||
VALUES('administrator','admin@discoverkeyser.com',?,1)",[$hash]);
|
||||
|
||||
/* Categories */
|
||||
foreach ([
|
||||
['Dining & Food','🍽️',1],['Shopping & Retail','🛍️',2],
|
||||
['Lodging','🏨',3],['Healthcare','🏥',4],['Education','🎓',5],
|
||||
['Financial Services','🏦',6],['Automotive','🔧',7],
|
||||
['Professional Services','💼',8],['Recreation & Fitness','⛹️',9],
|
||||
['Beauty & Personal Care','💇',10],['Government & Civic','🏛️',11],
|
||||
['Legal Services','⚖️',12],['Churches & Faith','⛪',13],
|
||||
] as $c) qrun("INSERT OR IGNORE INTO categories(name,icon,sort_order)VALUES(?,?,?)",$c);
|
||||
|
||||
$cid = fn(string $n): int => (int)qval("SELECT id FROM categories WHERE name=?",[$n]);
|
||||
$h = fn(array $d): string => json_encode($d);
|
||||
$std = $h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'Closed','Sun'=>'Closed']);
|
||||
$always= $h(['Mon'=>'Open 24 hrs','Tue'=>'Open 24 hrs','Wed'=>'Open 24 hrs','Thu'=>'Open 24 hrs','Fri'=>'Open 24 hrs','Sat'=>'Open 24 hrs','Sun'=>'Open 24 hrs']);
|
||||
|
||||
$D=$cid('Dining & Food'); $SH=$cid('Shopping & Retail'); $LO=$cid('Lodging');
|
||||
$HC=$cid('Healthcare'); $ED=$cid('Education'); $FI=$cid('Financial Services');
|
||||
$AU=$cid('Automotive'); $PS=$cid('Professional Services'); $RF=$cid('Recreation & Fitness');
|
||||
$BP=$cid('Beauty & Personal Care'); $GV=$cid('Government & Civic');
|
||||
$LG=$cid('Legal Services'); $CH=$cid('Churches & Faith');
|
||||
|
||||
$bi = $db->prepare("INSERT INTO businesses(name,slug,category_id,subcategory,description,address,phone,email,website,hours,lat,lng,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
|
||||
/* ── DINING ─────────────────────────────────────────────────── */
|
||||
$biz = [
|
||||
["Castiglia's Italian Eatery",'castiglia-italian',$D,'Italian Restaurant',
|
||||
"Keyser's most beloved restaurant with 16,000+ Facebook fans and 8,400+ visits recorded. Family recipes passed down for generations: handmade pasta, wood-fired dishes, classic antipasti, and decadent Italian desserts. Known for \"huge servings\" and amazing food. Wed–Thu 11am–9pm, Fri–Sat 11am–9pm, Sun noon–9pm.",
|
||||
'401 S. Mineral St., Keyser, WV 26726','304-788-1300','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–9pm','Sat'=>'11am–9pm','Sun'=>'12pm–9pm']),
|
||||
39.4340,-78.9734,1],
|
||||
|
||||
['The Candlewyck Inn','candlewyck-inn',$D,'Fine Dining',
|
||||
"Elegant fine dining in a beautifully restored historic setting. Upscale American cuisine with locally sourced seasonal ingredients. Award-winning wine list. Keyser's premier destination for special occasions and romantic dinners.",
|
||||
'65 S. Mineral St., Keyser, WV 26726','304-788-6594','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'5pm–9pm','Wed'=>'5pm–9pm','Thu'=>'5pm–9pm','Fri'=>'5pm–10pm','Sat'=>'4pm–10pm','Sun'=>'Closed']),
|
||||
39.4370,-78.9734,1],
|
||||
|
||||
["Clancy's Irish Pub",'clancys-irish-pub',$D,'Irish Pub & Bar',
|
||||
"Keyser's favorite neighborhood pub! Reviewers love the Jalo-Mango Boneless Wings. Cold craft beers on tap, hearty pub fare, sports on every screen, trivia nights on Wednesdays, and live music on weekends. Friendly welcoming atmosphere.",
|
||||
'777 Armstrong St., Keyser, WV 26726','304-788-1133','','',
|
||||
$h(['Mon'=>'4pm–12am','Tue'=>'4pm–12am','Wed'=>'4pm–12am','Thu'=>'4pm–12am','Fri'=>'4pm–2am','Sat'=>'12pm–2am','Sun'=>'12pm–10pm']),
|
||||
39.4390,-78.9750,1],
|
||||
|
||||
['North Branch Craft Pub','north-branch-craft-pub',$D,'Craft Brewery & Pub',
|
||||
"Keyser's craft beer destination. \"Bold flavors without being overly salty or greasy\" — raved by reviewers. Local and regional craft beers on rotating taps. Great pub fare with stunning North Branch river views. Live entertainment regularly. Sister company to Queens Point Coffee and North Branch Ventures.",
|
||||
'101 Armstrong St., Ste. 1, Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'4pm–11pm','Wed'=>'4pm–11pm','Thu'=>'4pm–11pm','Fri'=>'3pm–1am','Sat'=>'12pm–1am','Sun'=>'12pm–8pm']),
|
||||
39.4392,-78.9750,1],
|
||||
|
||||
['Queens Point Coffee','queens-point-coffee',$D,'Coffee Shop',
|
||||
"\"THE best coffee shop I've ever been to\" — visitor review. Named after the iconic Queens Point cliff overlooking Keyser. Iced matcha, lattes, specialty drinks, amazing food, friendly staff, cozy vibe, and merchandise. A must-visit. Sister company to North Branch Ventures and North Branch Pub.",
|
||||
'101 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'7am–3pm','Tue'=>'7am–5pm','Wed'=>'7am–5pm','Thu'=>'7am–5pm','Fri'=>'7am–5pm','Sat'=>'8am–5pm','Sun'=>'9am–2pm']),
|
||||
39.4392,-78.9750,1],
|
||||
|
||||
['Royal Restaurant','royal-restaurant',$D,'American / Comfort Food',
|
||||
"A Keyser classic visited by generations of families. \"Both my parents grew up in Keyser. Their food is amazing.\" Known for hearty breakfast, homemade pies, reuben sandwiches, and burgers. Many vegetarian options. Dine-in, take-out, and delivery available.",
|
||||
'88 N. Main St., Keyser, WV 26726','304-788-9825','','',
|
||||
$h(['Mon'=>'6am–8pm','Tue'=>'6am–8pm','Wed'=>'6am–8pm','Thu'=>'6am–8pm','Fri'=>'6am–9pm','Sat'=>'6am–9pm','Sun'=>'7am–3pm']),
|
||||
39.4378,-78.9734,0],
|
||||
|
||||
['Fat Bottom Grille','fat-bottom-grille',$D,'American Bar & Grill',
|
||||
"\"THE best place in town to get a burger or steak sub.\" Everything is always made fresh. \"The smash burger is juicy and flavored to perfection.\" Huge portions. \"The food is amazing and the price is excellent!!\" Tennessee whiskey burger and chili cheese fries are local legends. Do NOT miss this place.",
|
||||
'Keyser, WV 26726','304-788-0000','','',
|
||||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),
|
||||
39.4360,-78.9740,1],
|
||||
|
||||
["Denny's",'dennys',$D,'American / Diner',
|
||||
"Open 24/7. \"Suburb\" experience with professional management. America's diner at Route 220 South. Grand Slams, hearty burgers, and all-day breakfast. Reviewers say the recent improvements make it \"the Denny's we once loved.\"",
|
||||
'825 S. Mineral St. (US Rt. 220 S), Keyser, WV 26726','304-788-3090','','dennys.com',
|
||||
$always,39.4300,-78.9730,0],
|
||||
|
||||
["McDonald's",'mcdonalds',$D,'Fast Food',
|
||||
"The world's most visited fast food restaurant. Burgers, fries, breakfast sandwiches, McCafé beverages. Drive-through available at 700 S. Mineral St.",
|
||||
'700 S. Mineral St., Keyser, WV 26726','304-788-0604','','mcdonalds.com',
|
||||
$h(['Mon'=>'6am–11pm','Tue'=>'6am–11pm','Wed'=>'6am–11pm','Thu'=>'6am–11pm','Fri'=>'6am–12am','Sat'=>'6am–12am','Sun'=>'7am–11pm']),
|
||||
39.4310,-78.9730,0],
|
||||
|
||||
['Burger King','burger-king',$D,'Fast Food',
|
||||
'Home of the Whopper! Flame-grilled burgers and crispy fries. Drive-through available at RR #3 Box 3240 (New Creek Hwy). Quick and delicious.',
|
||||
'RR#3 Box 3240, New Creek Hwy, Keyser, WV 26726','304-788-0000','','burgerking.com',
|
||||
$h(['Mon'=>'6am–12am','Tue'=>'6am–12am','Wed'=>'6am–12am','Thu'=>'6am–12am','Fri'=>'6am–1am','Sat'=>'6am–1am','Sun'=>'7am–12am']),
|
||||
39.4285,-78.9720,0],
|
||||
|
||||
["Domino's Pizza",'dominos',$D,'Pizza / Delivery',
|
||||
'Hot fresh pizza fast. Carryout at 590 S. Mineral St. or delivered to your door. Online ordering available at dominos.com with live tracking.',
|
||||
'590 S. Mineral St., Keyser, WV 26726','304-788-6400','','dominos.com',
|
||||
$h(['Mon'=>'10:30am–12am','Tue'=>'10:30am–12am','Wed'=>'10:30am–12am','Thu'=>'10:30am–12am','Fri'=>'10:30am–1am','Sat'=>'10:30am–1am','Sun'=>'10:30am–12am']),
|
||||
39.4330,-78.9730,0],
|
||||
|
||||
['Dairy Queen Grill & Chill','dairy-queen',$D,'Ice Cream / Fast Food',
|
||||
"\"One of the best staff at any fast food restaurant. The ice cream and food is pretty great too.\" Signature Blizzard® treats, soft-serve cones, and grill burgers. \"Kid-friendliness: Ice cream, what's more kid-friendly than that?\" A Keyser summer tradition.",
|
||||
'460 S. Mineral St., Keyser, WV 26726','304-788-1499','','dairyqueen.com',
|
||||
$h(['Mon'=>'10am–10pm','Tue'=>'10am–10pm','Wed'=>'10am–10pm','Thu'=>'10am–10pm','Fri'=>'10am–10:30pm','Sat'=>'10am–10:30pm','Sun'=>'11am–9pm']),
|
||||
39.4320,-78.9730,0],
|
||||
|
||||
["Fox's Pizza Den",'foxs-pizza',$D,'Pizza',
|
||||
"\"Kirk Kesner is an amazing local business owner! He supports his community and runs a top notch pizza place! We are so fortunate to have this restaurant in our area!\" Hand-tossed pizzas, subs, wings. A true Keyser staple.",
|
||||
'567 S. Mineral St., Keyser, WV 26726','304-788-1149','','',
|
||||
$h(['Mon'=>'11am–10pm','Tue'=>'11am–10pm','Wed'=>'11am–10pm','Thu'=>'11am–10pm','Fri'=>'11am–11pm','Sat'=>'11am–11pm','Sun'=>'12pm–9pm']),
|
||||
39.4315,-78.9730,0],
|
||||
|
||||
['Little Caesars','little-caesars',$D,'Pizza / Fast Food',
|
||||
'Hot-N-Ready pizzas at unbeatable prices. No wait. Crazy Bread and Italian favorites. Quick and affordable for the whole family at 30 Armstrong St.',
|
||||
'30 Armstrong St., Keyser, WV 26726','304-788-7738','','littlecaesars.com',
|
||||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),
|
||||
39.4395,-78.9750,0],
|
||||
|
||||
['Subway (Inside Gulf)','subway',$D,'Sandwiches / Fast Food',
|
||||
'Fresh-built footlong subs, wraps, and salads at Route 3 Keyser location inside Gulf. "Bold new flavors and classic favorites every day."',
|
||||
'2 Heskiet St. (Gulf), Keyser, WV 26726','304-788-3613','','subway.com',
|
||||
$h(['Mon'=>'7am–10pm','Tue'=>'7am–10pm','Wed'=>'7am–10pm','Thu'=>'7am–10pm','Fri'=>'7am–10pm','Sat'=>'8am–10pm','Sun'=>'9am–9pm']),
|
||||
39.4260,-78.9720,0],
|
||||
|
||||
['Taco Bell','taco-bell',$D,'Mexican Fast Food',
|
||||
'Live Más! Find your nearest Taco Bell at 41 Plaza Drive. Tacos, burritos, nachos, and Crunchwrap Supremes at affordable prices. Drive-through available.',
|
||||
'41 Plaza Drive, Keyser, WV 26726','304-788-0000','','tacobell.com',
|
||||
$h(['Mon'=>'7am–12am','Tue'=>'7am–12am','Wed'=>'7am–12am','Thu'=>'7am–12am','Fri'=>'7am–2am','Sat'=>'7am–2am','Sun'=>'8am–12am']),
|
||||
39.4265,-78.9722,0],
|
||||
|
||||
["Ducky's Bar & Grill",'duckys-bar-grill',$D,'American Bar & Grill',
|
||||
"\"The ultimate combination of bar and grill. Come check out their new location in Keyser. The friendly staff will ensure you leave full and happy!\" Famous Tennessee whiskey burger and chili cheese fries. New Keyser location.",
|
||||
'Keyser, WV 26726','304-788-0000','','',
|
||||
$h(['Mon'=>'11am–11pm','Tue'=>'11am–11pm','Wed'=>'11am–11pm','Thu'=>'11am–11pm','Fri'=>'11am–1am','Sat'=>'11am–1am','Sun'=>'12pm–9pm']),
|
||||
39.4358,-78.9738,0],
|
||||
|
||||
["Hoover's Bar & Grill",'hoovers-bar-grill',$D,'Bar & Grill',
|
||||
'"If you enjoy smoked food, live entertainment on an outside stage, and cold beer, Hoover\'s is the place to be. A fun atmosphere with one-of-a-kind employees to make your trip fun!" Outdoor stage, smoked meats, great vibes.',
|
||||
'Keyser area, WV 26726','304-788-0000','','',
|
||||
$h(['Mon'=>'11am–10pm','Tue'=>'11am–10pm','Wed'=>'11am–10pm','Thu'=>'11am–10pm','Fri'=>'11am–12am','Sat'=>'11am–12am','Sun'=>'12pm–9pm']),
|
||||
39.4355,-78.9736,0],
|
||||
|
||||
['Long Spring Restaurant','long-spring',$D,'Chinese / American',
|
||||
'"I have been ordering from Long Spring for many years. I love their food." Popular for vegetable fried rice, shrimp dishes, and American favorites. Longtime Keyser community staple.',
|
||||
'Keyser, WV 26726','304-788-0000','','',
|
||||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),
|
||||
39.4362,-78.9736,0],
|
||||
|
||||
["Jin's Asian Cuisine 2",'jins-asian-cuisine',$D,'Asian / Chinese',
|
||||
"Fresh Chinese and Asian cuisine made to order. Located at 196 N. Tornado Way. Known for orange chicken, bourbon chicken, lo mein, fried rice, and crab rangoon.",
|
||||
'196 N. Tornado Way #11, Keyser, WV 26726','304-788-2222','','',
|
||||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),
|
||||
39.4355,-78.9730,0],
|
||||
|
||||
["Martie's Hot Dog Stand",'marties-hot-dogs',$D,'Hot Dogs / Street Food',
|
||||
"A beloved Keyser institution serving classic hot dogs with all the fixings for generations. Available on Uber Eats and DoorDash. Simple, delicious, and uniquely Keyser. A local landmark at 96 N. Main St.",
|
||||
'96 N. Main St., Keyser, WV 26726','304-788-7690','','',
|
||||
$h(['Mon'=>'10am–6pm','Tue'=>'10am–6pm','Wed'=>'10am–6pm','Thu'=>'10am–6pm','Fri'=>'10am–7pm','Sat'=>'10am–7pm','Sun'=>'Closed']),
|
||||
39.4375,-78.9734,0],
|
||||
|
||||
['M&S Desserts & Bakery','ms-desserts-bakery',$D,'Bakery / Desserts',
|
||||
"Artisan baked goods, custom cakes, pastries, and sweet treats made fresh daily. Keyser's premier bakery destination at 133 N. Main St.",
|
||||
'133 N. Main St., Keyser, WV 26726','304-788-3838','','',
|
||||
$h(['Mon'=>'7am–5pm','Tue'=>'7am–5pm','Wed'=>'7am–5pm','Thu'=>'7am–5pm','Fri'=>'7am–5pm','Sat'=>'8am–4pm','Sun'=>'Closed']),
|
||||
39.4374,-78.9734,0],
|
||||
|
||||
["Marla's Main Street Bakery",'marlas-bakery',$D,'Bakery',
|
||||
"Homestyle baked breads, rolls, and pastries made fresh. Located at 19 N. Main St. A delightful stop for fresh-baked goods any morning.",
|
||||
'19 N. Main St., Keyser, WV 26726','304-359-1348','','',
|
||||
$h(['Mon'=>'6am–4pm','Tue'=>'6am–4pm','Wed'=>'6am–4pm','Thu'=>'6am–4pm','Fri'=>'6am–4pm','Sat'=>'7am–3pm','Sun'=>'Closed']),
|
||||
39.4376,-78.9734,0],
|
||||
|
||||
['North Branch Brewing Co.','north-branch-brewing',$D,'Microbrewery',
|
||||
'"A start-up brewery offering other local breweries as well. There is a menu with a good selection of items with specials. Check out their Facebook page for live entertainment and special events." Local craft beers on tap.',
|
||||
'Keyser area, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'4pm–10pm','Thu'=>'4pm–10pm','Fri'=>'3pm–12am','Sat'=>'12pm–12am','Sun'=>'12pm–8pm']),
|
||||
39.4390,-78.9748,0],
|
||||
|
||||
['Happy Farmers Market','happy-farmers-market',$D,'Farmers Market / Local Produce',
|
||||
'Fresh local produce, seasonal items, and local goods. Located in the parking lot at 104 Shamrock Drive. Support local Mineral County farmers and producers.',
|
||||
'Parking Lot of 104 Shamrock Dr., Keyser, WV 26726','304-813-1576','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'Closed','Thu'=>'Closed','Fri'=>'4pm–7pm','Sat'=>'9am–1pm','Sun'=>'Closed']),
|
||||
39.4350,-78.9720,0],
|
||||
|
||||
['7-Eleven','seven-eleven',$D,'Convenience / Food',
|
||||
'Your neighborhood convenience store with hot food options, beverages, snacks, and everyday essentials. Located at 2 S. Mineral St.',
|
||||
'2 S. Mineral St., Keyser, WV 26726','304-788-0000','','7-eleven.com',
|
||||
$always,39.4364,-78.9734,0],
|
||||
|
||||
['Stop-By Grocery / Convenience','stop-by-grocery',$D,'Grocery / Convenience',
|
||||
'Local convenience and grocery store at 420 W. Piedmont St. Quick stop for everyday essentials, fresh items, and snacks.',
|
||||
'420 W. Piedmont St., Keyser, WV 26726','304-788-0123','','',
|
||||
$h(['Mon'=>'6am–10pm','Tue'=>'6am–10pm','Wed'=>'6am–10pm','Thu'=>'6am–10pm','Fri'=>'6am–10pm','Sat'=>'6am–10pm','Sun'=>'7am–9pm']),
|
||||
39.4372,-78.9748,0],
|
||||
|
||||
/* ── SHOPPING & RETAIL ──────────────────────────────────────── */
|
||||
["Wayne's Country Meats",'waynes-country-meats',$SH,'Butcher / Specialty Meats',
|
||||
'Premium quality meats from local farms. Custom cuts, specialty items, fresh jerky, and the widest selection of quality meats in Mineral County. A must-visit for meat lovers and BBQ enthusiasts at 670 Armstrong St.',
|
||||
'670 Armstrong St., Keyser, WV 26726','304-788-5956','','',
|
||||
$h(['Mon'=>'8am–6pm','Tue'=>'8am–6pm','Wed'=>'8am–6pm','Thu'=>'8am–6pm','Fri'=>'8am–6pm','Sat'=>'8am–5pm','Sun'=>'Closed']),
|
||||
39.4395,-78.9750,1],
|
||||
|
||||
['Save-A-Lot','save-a-lot',$SH,'Grocery',
|
||||
"Affordable grocery shopping for the whole family. Fresh produce, quality meats, dairy, and pantry essentials at everyday low prices. Keyser's budget-friendly grocery option on S. Mineral St.",
|
||||
'S. Mineral St., Keyser, WV 26726','304-788-7570','','save-a-lot.com',
|
||||
$h(['Mon'=>'8am–9pm','Tue'=>'8am–9pm','Wed'=>'8am–9pm','Thu'=>'8am–9pm','Fri'=>'8am–9pm','Sat'=>'8am–9pm','Sun'=>'9am–7pm']),
|
||||
39.4298,-78.9728,0],
|
||||
|
||||
['AutoZone','autozone',$SH,'Auto Parts',
|
||||
"America's leading auto parts retailer at 2 W. Piedmont St. Free battery testing, loaner tool program, and expert staff. Open every day.",
|
||||
'2 W. Piedmont St., Keyser, WV 26726','304-788-9058','','autozone.com',
|
||||
$h(['Mon'=>'7:30am–9pm','Tue'=>'7:30am–9pm','Wed'=>'7:30am–9pm','Thu'=>'7:30am–9pm','Fri'=>'7:30am–9pm','Sat'=>'7:30am–9pm','Sun'=>'9am–8pm']),
|
||||
39.4378,-78.9734,0],
|
||||
|
||||
['NAPA Auto Parts','napa-auto-parts',$SH,'Auto Parts',
|
||||
"The professional's choice for auto parts. Quality parts for all makes and models. Expert advice from knowledgeable staff.",
|
||||
'540 S. Mineral St., Keyser, WV 26726','304-788-3831','','napaonline.com',
|
||||
$h(['Mon'=>'8am–6pm','Tue'=>'8am–6pm','Wed'=>'8am–6pm','Thu'=>'8am–6pm','Fri'=>'8am–6pm','Sat'=>'8am–5pm','Sun'=>'Closed']),
|
||||
39.4317,-78.9730,0],
|
||||
|
||||
['Advance Auto Parts','advance-auto',$SH,'Auto Parts',
|
||||
'Quality auto parts and accessories. Battery testing, check-engine diagnostics, and wiper installation. Route 3 Keyser location.',
|
||||
'Rt. 3 Box 3134, Keyser, WV 26726','304-788-0011','','advanceautoparts.com',
|
||||
$h(['Mon'=>'7:30am–9pm','Tue'=>'7:30am–9pm','Wed'=>'7:30am–9pm','Thu'=>'7:30am–9pm','Fri'=>'7:30am–9pm','Sat'=>'7:30am–9pm','Sun'=>'9am–8pm']),
|
||||
39.4260,-78.9718,0],
|
||||
|
||||
['CVS Pharmacy','cvs',$SH,'Pharmacy / Health & Beauty',
|
||||
'Your neighborhood health and wellness destination at 45 S. Mineral St. Prescriptions, health products, beauty supplies, and household essentials.',
|
||||
'45 S. Mineral St., Keyser, WV 26726','304-788-3443','','cvs.com',
|
||||
$h(['Mon'=>'8am–9pm','Tue'=>'8am–9pm','Wed'=>'8am–9pm','Thu'=>'8am–9pm','Fri'=>'8am–9pm','Sat'=>'9am–6pm','Sun'=>'10am–6pm']),
|
||||
39.4368,-78.9734,0],
|
||||
|
||||
['AT&T Store','att-store',$SH,'Telecommunications',
|
||||
'Latest smartphones, tablets, and wireless plans at 862 S. Mineral St. Expert staff to find the right device and plan for your needs.',
|
||||
'862 S. Mineral St., Keyser, WV 26726','304-788-9000','','att.com',
|
||||
$h(['Mon'=>'10am–7pm','Tue'=>'10am–7pm','Wed'=>'10am–7pm','Thu'=>'10am–7pm','Fri'=>'10am–7pm','Sat'=>'10am–7pm','Sun'=>'12pm–5pm']),
|
||||
39.4292,-78.9728,0],
|
||||
|
||||
["Christy's Florist",'christys-florist',$SH,'Florist',
|
||||
'Fresh floral arrangements for every occasion — weddings, funerals, anniversaries, and celebrations. Delivery throughout Mineral County. 81 N. Main St.',
|
||||
'81 N. Main St., Keyser, WV 26726','304-788-3679','','',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'9am–3pm','Sun'=>'Closed']),
|
||||
39.4382,-78.9734,0],
|
||||
|
||||
['Southern States','southern-states',$SH,'Farm & Garden Supply',
|
||||
'Agricultural supplies, pet food, hardware, seed, fertilizer, and home and garden products. Serving Mineral County farmers and homeowners since 1923. 201 Patrick St.',
|
||||
'201 Patrick St., Keyser, WV 26726','304-788-2317','','southernstates.com',
|
||||
$h(['Mon'=>'7:30am–5:30pm','Tue'=>'7:30am–5:30pm','Wed'=>'7:30am–5:30pm','Thu'=>'7:30am–5:30pm','Fri'=>'7:30am–5:30pm','Sat'=>'8am–3pm','Sun'=>'Closed']),
|
||||
39.4410,-78.9755,0],
|
||||
|
||||
['Back Alley Crafts & Tarts','back-alley-crafts',$SH,'Crafts & Gifts',
|
||||
'Unique handmade crafts, locally made artisan goods, and specialty gifts at 226 S. Main St. Find one-of-a-kind treasures and support local artisans.',
|
||||
'226 S. Main St., Keyser, WV 26726','304-813-6256','','',
|
||||
$h(['Mon'=>'10am–5pm','Tue'=>'10am–5pm','Wed'=>'10am–5pm','Thu'=>'10am–5pm','Fri'=>'10am–6pm','Sat'=>'10am–4pm','Sun'=>'Closed']),
|
||||
39.4357,-78.9734,0],
|
||||
|
||||
['Heaven Sent Creations','heaven-sent-creations',$SH,'Gifts & Crafts',
|
||||
'Inspirational gifts, custom creations, home décor, and unique finds for every occasion at 117 Armstrong St.',
|
||||
'117 Armstrong St., Keyser, WV 26726','304-790-7402','','',
|
||||
$h(['Mon'=>'10am–5pm','Tue'=>'10am–5pm','Wed'=>'10am–5pm','Thu'=>'10am–5pm','Fri'=>'10am–5pm','Sat'=>'10am–4pm','Sun'=>'Closed']),
|
||||
39.4393,-78.9750,0],
|
||||
|
||||
['Second Hand City','second-hand-city',$SH,'Thrift / Secondhand',
|
||||
"Keyser's thrift store at 232 S. Main St. Great deals on clothing, housewares, furniture, electronics, and more. New inventory arriving regularly.",
|
||||
'232 S. Main St., Keyser, WV 26726','304-788-4088','','',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'9am–5pm','Sun'=>'Closed']),
|
||||
39.4357,-78.9734,0],
|
||||
|
||||
['S&H Variety','sh-variety',$SH,'Variety Store',
|
||||
'Your local variety store for household items, gifts, seasonal décor, and everyday essentials at 79 N. Main St. Friendly local service.',
|
||||
'79 N. Main St., Keyser, WV 26726','304-788-1800','','',
|
||||
$h(['Mon'=>'9am–5:30pm','Tue'=>'9am–5:30pm','Wed'=>'9am–5:30pm','Thu'=>'9am–5:30pm','Fri'=>'9am–6pm','Sat'=>'9am–5pm','Sun'=>'Closed']),
|
||||
39.4380,-78.9734,0],
|
||||
|
||||
['Say It With Thread Embroidery & Gifts','say-it-with-thread',$SH,'Embroidery / Custom Gifts',
|
||||
'Custom embroidery, personalized gifts, and apparel at 527 S. Mineral St. Perfect for business logos, team gear, and unique personalized items.',
|
||||
'527 S. Mineral St., Keyser, WV 26726','304-788-4739','','',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'10am–3pm','Sun'=>'Closed']),
|
||||
39.4318,-78.9730,0],
|
||||
|
||||
['Nu View Video & Tobacco','nu-view-video-tobacco',$SH,'Video & Tobacco',
|
||||
'Videos, tobacco products, and related accessories at 397 New Creek Hwy. Local retailer serving Keyser.',
|
||||
'397 New Creek Hwy, Keyser, WV 26726','304-788-1769','','',
|
||||
$h(['Mon'=>'10am–8pm','Tue'=>'10am–8pm','Wed'=>'10am–8pm','Thu'=>'10am–8pm','Fri'=>'10am–9pm','Sat'=>'10am–9pm','Sun'=>'12pm–7pm']),
|
||||
39.4285,-78.9718,0],
|
||||
|
||||
['Thunder Hill Outfitters','thunder-hill-outfitters',$SH,'Hunting & Fishing Supplies',
|
||||
"Hunting and fishing supplies, gear, licenses, and expert local knowledge of Mineral County's outdoors. Located at 105 Armstrong St.",
|
||||
'105 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–7pm','Sat'=>'8am–7pm','Sun'=>'10am–4pm']),
|
||||
39.4392,-78.9750,0],
|
||||
|
||||
["Robin's Country Crafts",'robins-country-crafts',$SH,'Country Crafts & Décor',
|
||||
'Charming country crafts, home décor, and unique gifts at 370 S. Mineral St. Mountain-country flair and one-of-a-kind finds.',
|
||||
'370 S. Mineral St., Keyser, WV 26726','304-788-0445','','',
|
||||
$h(['Mon'=>'10am–5pm','Tue'=>'10am–5pm','Wed'=>'10am–5pm','Thu'=>'10am–5pm','Fri'=>'10am–5pm','Sat'=>'10am–4pm','Sun'=>'Closed']),
|
||||
39.4340,-78.9732,0],
|
||||
|
||||
['West Virginia Outdoor Power','wv-outdoor-power',$SH,'Outdoor Power Equipment',
|
||||
'Sales and service of outdoor power equipment — lawn mowers, chainsaws, generators, and more. Authorized service center at 580 S. Mineral St.',
|
||||
'580 S. Mineral St., Keyser, WV 26726','304-788-0008','','',
|
||||
$h(['Mon'=>'8am–5pm','Tue'=>'8am–5pm','Wed'=>'8am–5pm','Thu'=>'8am–5pm','Fri'=>'8am–5pm','Sat'=>'8am–12pm','Sun'=>'Closed']),
|
||||
39.4312,-78.9730,0],
|
||||
|
||||
['Urice Supply','urice-supply',$SH,'Building Supply',
|
||||
'Building materials, hardware, and supply needs at 465 Armstrong St. Serving Mineral County contractors and homeowners.',
|
||||
'465 Armstrong St., Keyser, WV 26726','304-788-3215','','',
|
||||
$h(['Mon'=>'7:30am–5pm','Tue'=>'7:30am–5pm','Wed'=>'7:30am–5pm','Thu'=>'7:30am–5pm','Fri'=>'7:30am–5pm','Sat'=>'8am–12pm','Sun'=>'Closed']),
|
||||
39.4395,-78.9752,0],
|
||||
|
||||
['Premier Rental Purchase','premier-rental',$SH,'Rent-to-Own',
|
||||
'Rent-to-own furniture, electronics, and appliances at 140 Keyser Mall. Flexible payments, no credit check, quality name brands.',
|
||||
'140 Keyser Mall, Keyser, WV 26726','304-790-7136','','',
|
||||
$h(['Mon'=>'9am–7pm','Tue'=>'9am–7pm','Wed'=>'9am–7pm','Thu'=>'9am–7pm','Fri'=>'9am–7pm','Sat'=>'9am–5pm','Sun'=>'Closed']),
|
||||
39.4355,-78.9720,0],
|
||||
|
||||
/* ── LODGING ─────────────────────────────────────────────────── */
|
||||
['SureStay Plus by Best Western Keyser','surestay-best-western',$LO,'Hotel',
|
||||
'Comfortable hotel in the scenic Potomac Highlands. Free hot breakfast daily, free WiFi, indoor pool, and fitness center. Pet-friendly. Rated 3/5 stars. Convenient to PSC, Potomac Valley Hospital, and all area attractions. Best Western-branded quality at affordable prices.',
|
||||
'New Creek Hwy, Keyser, WV 26726','304-788-0000','','bestwestern.com',
|
||||
$always,39.4298,-78.9705,1],
|
||||
|
||||
['Keyser Inn','keyser-inn',$LO,'Motel',
|
||||
'Affordable, clean accommodations in a convenient Keyser location. Continental breakfast, free WiFi, and pet-friendly policy. Great base for PSC visits and area attractions.',
|
||||
'Keyser, WV 26726','304-788-0000','','',
|
||||
$always,39.4360,-78.9734,0],
|
||||
|
||||
/* ── HEALTHCARE ─────────────────────────────────────────────── */
|
||||
['WVU Medicine Potomac Valley Hospital','pvh',$HC,'Hospital / Medical Center',
|
||||
'Full-service community hospital and part of the WVU Medicine health system. Emergency department, surgical services, maternity care, cardiac care, comprehensive outpatient clinics. Located at 100 Pin Oak Ln.',
|
||||
'100 Pin Oak Ln., Keyser, WV 26726','304-597-3500','','wvumedicine.org',
|
||||
$always,39.4275,-78.9725,1],
|
||||
|
||||
['Med-a-Save Pharmacy','med-a-save',$HC,'Independent Pharmacy',
|
||||
"Local independent pharmacy at 818 S. Mineral St. Personalized service, prescription filling, compounding, and immunizations. The local choice when you want pharmacists who know your name.",
|
||||
'818 S. Mineral St., Keyser, WV 26726','304-788-6010','','',
|
||||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–6pm','Sat'=>'9am–1pm','Sun'=>'Closed']),
|
||||
39.4287,-78.9728,0],
|
||||
|
||||
['Recovery Care LLC','recovery-care',$HC,'Recovery / Counseling',
|
||||
'Providing compassionate recovery care and counseling services for individuals and families in Mineral County. 37 Carroll Ave.',
|
||||
'37 Carroll Ave., Keyser, WV 26726','304-790-7467','','',
|
||||
$std,39.4360,-78.9738,0],
|
||||
|
||||
/* ── EDUCATION ──────────────────────────────────────────────── */
|
||||
['Potomac State College of WVU','potomac-state-college',$ED,'Community College / University',
|
||||
"WVU Potomac State College — the WVU campus in Keyser's Eastern Panhandle, founded 1901 on historic Fort Fuller (Civil War site commanded by future president Benjamin Harrison and Lew Wallace, author of Ben-Hur). Associate and bachelor's degrees in 40+ programs. Home of the Catamounts.",
|
||||
'101 Fort Ave., Keyser, WV 26726','304-788-6800','','potomacstatecollege.edu',
|
||||
$std,39.4368,-78.9715,1],
|
||||
|
||||
['Mineral County Schools','mineral-county-schools',$ED,'Public School System',
|
||||
'Serving Mineral County through Keyser High School, Keyser Middle School, Keyser Primary, and more. Mineral County Board of Education at 40 Fir Street.',
|
||||
'40 Fir Street, Keyser, WV 26726','304-788-4200','','mineralcountyboe.com',
|
||||
$std,39.4370,-78.9738,0],
|
||||
|
||||
['Mineral County Technical Center','mineral-county-tech',$ED,'Vocational / Technical School',
|
||||
'Career and technical education for high school students and adults. Programs in welding, cosmetology, culinary arts, construction trades, healthcare, and information technology.',
|
||||
'Keyser, WV 26726','304-788-5890','','',
|
||||
$std,39.4362,-78.9730,0],
|
||||
|
||||
['Mineral County Public Library','mineral-county-library',$ED,'Public Library',
|
||||
'Community hub for knowledge, culture, and lifelong learning. Books, digital resources, internet access, printing, and community programs for all ages.',
|
||||
'Keyser, WV 26726','304-788-3100','','',
|
||||
$h(['Mon'=>'10am–7pm','Tue'=>'10am–7pm','Wed'=>'10am–7pm','Thu'=>'10am–7pm','Fri'=>'10am–5pm','Sat'=>'10am–4pm','Sun'=>'Closed']),
|
||||
39.4370,-78.9734,0],
|
||||
|
||||
/* ── FINANCIAL SERVICES ─────────────────────────────────────── */
|
||||
['First United Bank & Trust','first-united-bank',$FI,'Community Bank',
|
||||
'Community banking rooted in West Virginia. Personal checking, savings, mortgages, business accounts. 29 West Southern Dr.',
|
||||
'29 West Southern Dr., Keyser, WV 26726','304-788-2552','','mybank.com',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5:30pm','Sat'=>'9am–12pm','Sun'=>'Closed']),
|
||||
39.4372,-78.9740,0],
|
||||
|
||||
['M&T Bank','m-t-bank',$FI,'Bank',
|
||||
'Full-service personal, small business, and commercial banking. 24/7 ATM at 67 N. Main St. Online and mobile banking available.',
|
||||
'67 N. Main St., Keyser, WV 26726','304-788-6782','','mtb.com',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'9am–12pm','Sun'=>'Closed']),
|
||||
39.4380,-78.9734,0],
|
||||
|
||||
['BB&T Bank (Truist)','truist-bank',$FI,'Bank',
|
||||
'Now Truist Bank. Full-service banking with personal, business, and mortgage products. 220 S. Eagle Ln. ATM on premises.',
|
||||
'220 S. Eagle Ln., Keyser, WV 26726','304-788-3111','','truist.com',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5:30pm','Sat'=>'9am–12pm','Sun'=>'Closed']),
|
||||
39.4362,-78.9732,0],
|
||||
|
||||
['H&R Block','hr-block',$FI,'Tax Preparation',
|
||||
'Professional tax preparation at 66 N. Main St. In-person and virtual filing. Maximum refund guaranteed.',
|
||||
'66 N. Main St., Keyser, WV 26726','304-788-2926','','hrblock.com',
|
||||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–6pm','Sat'=>'9am–5pm','Sun'=>'Closed']),
|
||||
39.4380,-78.9734,0],
|
||||
|
||||
['Jackson Hewitt','jackson-hewitt',$FI,'Tax Preparation',
|
||||
'Tax preparation services at 77 N. Main St. Expert help filing your federal and state taxes. Refund advance available.',
|
||||
'77 N. Main St., Keyser, WV 26726','304-788-5650','','jacksonhewitt.com',
|
||||
$h(['Mon'=>'9am–7pm','Tue'=>'9am–7pm','Wed'=>'9am–7pm','Thu'=>'9am–7pm','Fri'=>'9am–7pm','Sat'=>'9am–6pm','Sun'=>'Closed']),
|
||||
39.4380,-78.9734,0],
|
||||
|
||||
['Liberty Tax Service','liberty-tax',$FI,'Tax Preparation',
|
||||
'Tax preparation services at 690 S. Mineral St., Suite A. Filing for individuals and businesses. Fast refunds available.',
|
||||
'690 S. Mineral St. Suite A, Keyser, WV 26726','304-405-2572','','libertytax.com',
|
||||
$h(['Mon'=>'9am–7pm','Tue'=>'9am–7pm','Wed'=>'9am–7pm','Thu'=>'9am–7pm','Fri'=>'9am–7pm','Sat'=>'9am–5pm','Sun'=>'Closed']),
|
||||
39.4305,-78.9730,0],
|
||||
|
||||
['Tax Time','tax-time',$FI,'Tax Preparation',
|
||||
'Local tax preparation services at 528 S. Mineral St. Helping Keyser residents file their taxes accurately and affordably.',
|
||||
'528 S. Mineral St., Keyser, WV 26726','304-788-1947','','',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'9am–3pm','Sun'=>'Closed']),
|
||||
39.4320,-78.9730,0],
|
||||
|
||||
/* ── AUTOMOTIVE ─────────────────────────────────────────────── */
|
||||
["Boddy's Automotive",'boddys-auto',$AU,'Full Service Auto Repair',
|
||||
'Trusted local auto repair for all makes and models at 220 Armstrong St. Engine, transmission, brakes, alignment, tires, and electrical. Quality work at honest prices.',
|
||||
'220 Armstrong St., Keyser, WV 26726','304-788-5511','','',
|
||||
$h(['Mon'=>'8am–5pm','Tue'=>'8am–5pm','Wed'=>'8am–5pm','Thu'=>'8am–5pm','Fri'=>'8am–5pm','Sat'=>'Closed','Sun'=>'Closed']),
|
||||
39.4392,-78.9750,0],
|
||||
|
||||
['Gimme A Brake Auto','gimme-a-brake',$AU,'Brakes / Tires / Oil Changes',
|
||||
'Fast, friendly, and affordable auto care at 375 W. Piedmont St. Brakes, tires, oil changes, and full mechanical service. No appointment needed for routine maintenance.',
|
||||
'375 West Piedmont St., Keyser, WV 26726','304-788-7810','','',
|
||||
$h(['Mon'=>'8am–5:30pm','Tue'=>'8am–5:30pm','Wed'=>'8am–5:30pm','Thu'=>'8am–5:30pm','Fri'=>'8am–5:30pm','Sat'=>'8am–12pm','Sun'=>'Closed']),
|
||||
39.4378,-78.9748,0],
|
||||
|
||||
['Cell Phone Repair Near Me','cell-phone-repair',$AU,'Electronics Repair',
|
||||
'Fast and affordable smartphone, tablet, and device repair at 82 N. Main St. Screen repairs, battery replacements, water damage, and more.',
|
||||
'82 N. Main St., Keyser, WV 26726','304-209-8375','','',
|
||||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–6pm','Sat'=>'10am–4pm','Sun'=>'Closed']),
|
||||
39.4380,-78.9734,0],
|
||||
|
||||
/* ── PROFESSIONAL SERVICES ──────────────────────────────────── */
|
||||
['Boggs Supply and Rental Center','boggs-supply',$PS,'Equipment Rental / Supply',
|
||||
'Tools, equipment, and supplies for rent or purchase at 464 Harley O Staggers Sr. Dr. Home improvement, construction, landscaping, and event equipment.',
|
||||
'464 Harley O Staggers Sr. Dr., Keyser, WV 26726','304-788-1617','','',
|
||||
$h(['Mon'=>'7:30am–5pm','Tue'=>'7:30am–5pm','Wed'=>'7:30am–5pm','Thu'=>'7:30am–5pm','Fri'=>'7:30am–5pm','Sat'=>'8am–12pm','Sun'=>'Closed']),
|
||||
39.4382,-78.9748,0],
|
||||
|
||||
['Crystal Clear Services of WV','crystal-clear',$PS,'Cleaning Services',
|
||||
'Professional commercial and residential cleaning services at 237 Lincoln St. Reliable, thorough, and detail-oriented. Serving Keyser and Mineral County.',
|
||||
'237 Lincoln St., Keyser, WV 26726','304-788-2119','','',
|
||||
$std,39.4368,-78.9730,0],
|
||||
|
||||
/* ── LEGAL SERVICES ─────────────────────────────────────────── */
|
||||
['Legal Aid of WV','legal-aid',$LG,'Legal Aid',
|
||||
'Free civil legal aid for eligible low-income West Virginians at 251 W. Piedmont St. Serving Mineral County with housing, family law, consumer, and benefits issues.',
|
||||
'251 W. Piedmont St., Keyser, WV 26726','304-788-6770','','lawv.net',
|
||||
$std,39.4374,-78.9750,0],
|
||||
|
||||
['Staggers & Staggers','staggers-staggers',$LG,'Law Office',
|
||||
'Legal representation for Mineral County at 190 Center St. Personal injury, real estate, estate planning, and general practice.',
|
||||
'190 Center St., Keyser, WV 26726','304-788-5749','','',
|
||||
$std,39.4370,-78.9738,0],
|
||||
|
||||
['Collins Law Firm','collins-law',$LG,'Law Office',
|
||||
'Legal services for Mineral County residents at 155 Armstrong St. Handling diverse civil and legal matters.',
|
||||
'155 Armstrong St., Keyser, WV 26726','304-788-2582','','',
|
||||
$std,39.4392,-78.9750,0],
|
||||
|
||||
/* ── RECREATION & FITNESS ───────────────────────────────────── */
|
||||
['North Branch Ventures','north-branch-ventures',$RF,'Kayak Rental / Outdoor Recreation',
|
||||
"\"We have enjoyed our local river and wish to share that with others.\" Kayak and river supply rentals on the beautiful North Branch Potomac. Guided river experiences, fishing supplies. Listed on TripAdvisor's top 5 things to do in Keyser. Sister company to Queens Point Coffee and North Branch Pub.",
|
||||
'101 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'10am–6pm','Thu'=>'10am–6pm','Fri'=>'9am–7pm','Sat'=>'8am–7pm','Sun'=>'8am–5pm']),
|
||||
39.4392,-78.9750,1],
|
||||
|
||||
['PSC Recreation Center','psc-rec-center',$RF,'Fitness Center / Pool',
|
||||
"\"Full Service Fitness Center with indoor pool, hot tub, strength and aerobics, group exercise classes, and more! We offer annual, semester and daily rates.\" Listed on TripAdvisor's top 5 things to do in Keyser. Open to the public.",
|
||||
'J. Edward Kelley Complex, 101 Fort Ave., Keyser, WV','304-788-6800','','potomacstatecollege.edu',
|
||||
$h(['Mon'=>'6am–9pm','Tue'=>'6am–9pm','Wed'=>'6am–9pm','Thu'=>'6am–9pm','Fri'=>'6am–8pm','Sat'=>'8am–6pm','Sun'=>'12pm–6pm']),
|
||||
39.4368,-78.9715,0],
|
||||
|
||||
['Mineral County Parks & Recreation','mineral-county-parks',$RF,'Parks & Recreation',
|
||||
"County-managed parks including Larenim Park — 365 acres with two pavilions (10 tables each), amphitheater (600 seating), sports fields, approximately 5 miles of trails, two fishing ponds, and an arboretum under construction. Also includes New Creek Trail with 4 miles of rail/trail and 1 mile of river frontage.",
|
||||
'Keyser, WV 26726','304-788-3066','','mineralwv.gov',
|
||||
$h(['Mon'=>'Open Daily','Tue'=>'Open Daily','Wed'=>'Open Daily','Thu'=>'Open Daily','Fri'=>'Open Daily','Sat'=>'Open Daily','Sun'=>'Open Daily']),
|
||||
39.4350,-78.9720,0],
|
||||
|
||||
['Polish Pines Golf Course','polish-pines-golf',$RF,'Golf Course',
|
||||
'Scenic public golf course set amid the rolling West Virginia hills near Keyser. Well-maintained fairways, clubhouse, and equipment rentals. Open to all skill levels.',
|
||||
'Keyser area, WV 26726','304-788-0000','','',
|
||||
$h(['Mon'=>'Dawn–Dusk','Tue'=>'Dawn–Dusk','Wed'=>'Dawn–Dusk','Thu'=>'Dawn–Dusk','Fri'=>'Dawn–Dusk','Sat'=>'Dawn–Dusk','Sun'=>'Dawn–Dusk']),
|
||||
39.4420,-78.9700,0],
|
||||
|
||||
/* ── BEAUTY & PERSONAL CARE ─────────────────────────────────── */
|
||||
['Eclips Hair Salon','eclips',$BP,'Hair Salon',
|
||||
'Full-service hair salon at 153 S. Mineral St. Cuts, color, styling, and treatments for men and women.',
|
||||
'153 S. Mineral St., Keyser, WV 26726','304-788-5578','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'8am–4pm','Sun'=>'Closed']),
|
||||
39.4352,-78.9733,0],
|
||||
|
||||
['Hair Depot','hair-depot',$BP,'Hair Salon / Barber',
|
||||
'Haircuts, styling, and grooming for the whole family at 41 Ward Ave. Affordable prices and friendly service.',
|
||||
'41 Ward Ave., Keyser, WV 26726','304-788-9100','','',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'8am–4pm','Sun'=>'Closed']),
|
||||
39.4360,-78.9725,0],
|
||||
|
||||
['In The Skin II','in-the-skin',$BP,'Tattoo & Body Art',
|
||||
'Professional tattoo and body art studio at 129 N. Main St. Custom designs, cover-ups, and piercings by experienced artists.',
|
||||
'129 N. Main St., Keyser, WV 26726','304-597-2070','','',
|
||||
$h(['Mon'=>'11am–7pm','Tue'=>'11am–7pm','Wed'=>'11am–7pm','Thu'=>'11am–7pm','Fri'=>'11am–8pm','Sat'=>'10am–6pm','Sun'=>'Closed']),
|
||||
39.4373,-78.9734,0],
|
||||
|
||||
["Mandi's Mane Street Studio",'mandis-salon',$BP,'Hair Salon',
|
||||
"Full-service salon at 92 N. Main St. Cuts, color, perms, highlights, and special occasion styling for all ages.",
|
||||
'92 N. Main St., Keyser, WV 26726','304-788-6634','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'8am–3pm','Sun'=>'Closed']),
|
||||
39.4379,-78.9734,0],
|
||||
|
||||
/* ── GOVERNMENT & CIVIC ─────────────────────────────────────── */
|
||||
['Mineral County Courthouse','mineral-county-courthouse',$GV,'County Government',
|
||||
"Listed on the National Register of Historic Places, this 1868 courthouse is the architectural crown jewel of downtown Keyser. Houses all county offices including clerk's office, assessor, county commission, and circuit court.",
|
||||
'Keyser, WV 26726','304-788-3003','','mineralwv.gov',
|
||||
$h(['Mon'=>'8:30am–4:30pm','Tue'=>'8:30am–4:30pm','Wed'=>'8:30am–4:30pm','Thu'=>'8:30am–4:30pm','Fri'=>'8:30am–4:30pm','Sat'=>'Closed','Sun'=>'Closed']),
|
||||
39.4385,-78.9738,0],
|
||||
|
||||
['City of Keyser','city-of-keyser',$GV,'City Government',
|
||||
"Official offices of the City of Keyser, WV. City council, mayor's office, utilities, and municipal services for the county seat of Mineral County.",
|
||||
'Keyser, WV 26726','304-788-0222','','cityofkeyser.com',
|
||||
$h(['Mon'=>'8:30am–4:30pm','Tue'=>'8:30am–4:30pm','Wed'=>'8:30am–4:30pm','Thu'=>'8:30am–4:30pm','Fri'=>'8:30am–4:30pm','Sat'=>'Closed','Sun'=>'Closed']),
|
||||
39.4375,-78.9734,0],
|
||||
|
||||
['Mineral County Tourism Visitors Center','mineral-county-tourism',$GV,'Tourism / Visitors Center',
|
||||
'167 S. Mineral Street — Your starting point for exploring Mineral County! Brochures, maps, event information, and friendly staff. "We strive to promote all sectors of tourism including beautiful views, outdoor recreation, delicious food, and rich local history." Call 304-790-7081 or email mineralcocvb@gmail.com.',
|
||||
'167 S. Mineral St., Keyser, WV 26726','304-790-7081','mineralcocvb@gmail.com','govisitmineralwv.com',
|
||||
$h(['Mon'=>'9am–4pm','Tue'=>'9am–4pm','Wed'=>'9am–4pm','Thu'=>'9am–4pm','Fri'=>'9am–4pm','Sat'=>'Call Ahead','Sun'=>'Closed']),
|
||||
39.4358,-78.9734,1],
|
||||
|
||||
['Mineral County Historical Society Museum','mineral-county-historical',$GV,'Museum / Historical Society',
|
||||
'"Recently opened, this museum tracks the history of Mineral County and Keyser, WV with displays of its people, its history, and its culture." Listed on TripAdvisor\'s top attractions. Open by appointment only and on special days — please call before visiting.',
|
||||
'Keyser, WV 26726','304-788-3066','','',
|
||||
$h(['Mon'=>'By Appt','Tue'=>'By Appt','Wed'=>'By Appt','Thu'=>'By Appt','Fri'=>'By Appt','Sat'=>'Special Events','Sun'=>'Special Events']),
|
||||
39.4378,-78.9734,0],
|
||||
|
||||
/* ── CHURCHES ───────────────────────────────────────────────── */
|
||||
['Keyser First United Methodist Church','keyser-umc',$CH,'Methodist Church',
|
||||
'Welcoming United Methodist congregation at 32 N. Davis St. serving Keyser since the 19th century. Sunday worship, community programs, and outreach ministries.',
|
||||
'32 N. Davis St., Keyser, WV 26726','304-788-2522','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'Closed','Thu'=>'Closed','Fri'=>'Closed','Sat'=>'Closed','Sun'=>'Worship 11am']),
|
||||
39.4372,-78.9730,0],
|
||||
|
||||
['Living Faith Fellowship','living-faith',$CH,'Non-Denominational Church',
|
||||
'Vibrant non-denominational Christian fellowship at 1 N. Main St. Contemporary worship, small groups, and community ministry. Wednesday evening service.',
|
||||
'1 N. Main St., Keyser, WV 26726','304-788-1910','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'7pm','Thu'=>'Closed','Fri'=>'Closed','Sat'=>'Closed','Sun'=>'Worship 10:30am']),
|
||||
39.4376,-78.9734,0],
|
||||
|
||||
['Trinity Lutheran Church','trinity-lutheran',$CH,'Lutheran Church',
|
||||
'ELCA Lutheran congregation at 76 N. Davis St. #2 with a long history serving the Keyser community. All are welcome.',
|
||||
'76 N. Davis St. #2, Keyser, WV 26726','304-788-3200','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'Closed','Thu'=>'Closed','Fri'=>'Closed','Sat'=>'Closed','Sun'=>'Worship 9am']),
|
||||
39.4372,-78.9730,0],
|
||||
];
|
||||
|
||||
// Fix the church row that accidentally duplicated category_id lookup
|
||||
foreach ($biz as &$row) {
|
||||
if (isset($row[2]) && is_string($row[2])) $row[2] = $cid($row[2]);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
foreach ($biz as $r) $bi->execute($r);
|
||||
|
||||
/* ── MENUS ──────────────────────────────────────────────────── */
|
||||
$si = $db->prepare("INSERT INTO menu_sections(business_id,name,description,sort_order)VALUES(?,?,?,?)");
|
||||
$mi = $db->prepare("INSERT INTO menu_items(section_id,name,description,price,sort_order)VALUES(?,?,?,?,?)");
|
||||
|
||||
// Castiglia's
|
||||
$castId = (int)qval("SELECT id FROM businesses WHERE slug='castiglia-italian'");
|
||||
foreach ([['Antipasti & Salads','Fresh starters',1],['Pasta','Handmade pastas',2],['Pizza','Stone-baked Italian pizzas',3],['Entrees','Secondi piatti',4],['Desserts','Dolci — sweet finishes',5]] as $s)
|
||||
$si->execute(array_merge([$castId],$s));
|
||||
$cs=[]; foreach(qall("SELECT id,name FROM menu_sections WHERE business_id=$castId") as $r) $cs[$r['name']]=$r['id'];
|
||||
foreach(['Antipasti & Salads'=>[['Bruschetta al Pomodoro','Toasted rustic bread with vine-ripened tomatoes, fresh basil & extra-virgin olive oil',8.95,1],['Calamari Fritti','Lightly breaded fresh calamari, marinara sauce & lemon aioli',13.95,2],['Caprese Salad','Fresh buffalo mozzarella, heirloom tomatoes, basil & balsamic glaze',11.95,3],['Minestrone Soup','Classic Italian vegetable soup with cannellini beans',7.50,4],['Caesar Salad','Romaine, house Caesar dressing, shaved Parmigiano, croutons',9.95,5]],
|
||||
'Pasta'=>[['Spaghetti Carbonara','Classic Roman — guanciale, egg, Pecorino Romano, cracked black pepper',15.95,1],['Fettuccine Alfredo','House-made fettuccine in rich cream & Parmigiano sauce',14.95,2],['Lasagna al Forno','House-rolled pasta, Bolognese, béchamel & Parmigiano',16.95,3],['Baked Ziti','Ziti, house marinara, ricotta & melted mozzarella',13.95,4],['Penne Arrabbiata','Spicy tomato, garlic, Calabrian chiles & fresh parsley',12.95,5],['Cacio e Pepe','Spaghetti, Pecorino Romano, Parmigiano Reggiano & abundant black pepper',14.50,6]],
|
||||
'Pizza'=>[['Margherita','San Marzano tomatoes, fior di latte mozzarella & fresh basil',14.95,1],['Pepperoni','House marinara, mozzarella & premium pepperoni',15.95,2],['Quattro Stagioni','Ham, mushrooms, artichoke hearts & Kalamata olives',17.95,3],['Prosciutto & Arugula','Parma prosciutto, baby arugula & shaved Parmigiano',18.95,4],['Funghi Truffle','Wild mushrooms, truffle oil, taleggio & fresh thyme',19.95,5]],
|
||||
'Entrees'=>[['Chicken Parmigiana','Breaded chicken, house marinara & fresh mozzarella, served with pasta',18.95,1],['Osso Buco alla Milanese','Braised veal shank, saffron risotto & traditional gremolata',34.95,2],['Eggplant Parmigiana','Layered eggplant, San Marzano sauce & mozzarella — vegetarian',15.95,3],['Salmon Piccata','Pan-seared Atlantic salmon, lemon-caper butter & fresh herbs',24.95,4],['Bistecca Fiorentina','Grilled 16oz T-bone, rosemary salt & roasted vegetables',42.95,5]],
|
||||
'Desserts'=>[['Tiramisu','Espresso-soaked ladyfingers, mascarpone cream & cocoa — our signature',8.95,1],['Cannoli Siciliani','Crispy shells with sweet ricotta, chocolate chips & pistachios',7.95,2],['Panna Cotta','Silky vanilla panna cotta with seasonal berry coulis',7.50,3],['Gelato del Giorno','Two scoops house-made gelato — ask your server for today\'s flavors',6.95,4],['Affogato','Vanilla gelato "drowned" in a shot of espresso',6.50,5]]]
|
||||
as $sec=>$items) foreach($items as $item) $mi->execute(array_merge([$cs[$sec]],$item));
|
||||
|
||||
// Queens Point Coffee
|
||||
$qpId = (int)qval("SELECT id FROM businesses WHERE slug='queens-point-coffee'");
|
||||
foreach ([['Espresso Drinks','Hot & cold espresso beverages',1],['Cold Drinks','Refreshing chilled options',2],['Food','Fresh baked goods & light bites',3],['Teas','Loose-leaf & herbal selections',4]] as $s)
|
||||
$si->execute(array_merge([$qpId],$s));
|
||||
$qs=[]; foreach(qall("SELECT id,name FROM menu_sections WHERE business_id=$qpId") as $r) $qs[$r['name']]=$r['id'];
|
||||
foreach(['Espresso Drinks'=>[['Latte','Smooth espresso with steamed milk',4.50,1],['Cappuccino','Double espresso with foamed milk',4.25,2],['Americano','Espresso shots with hot water',3.50,3],['Mocha','Espresso, chocolate & steamed milk',5.00,4],['Flat White','Ristretto shots with velvety microfoam',4.75,5],['Iced Matcha Latte','Matcha with milk & choice of flavor — our most raved drink!',5.25,6]],
|
||||
'Cold Drinks'=>[['Cold Brew','Smooth 24-hour cold brew over ice',3.75,1],['Iced Latte','Chilled espresso with cold milk',4.75,2],['Blended Frappe','Blended ice coffee drink',5.50,3],['Iced Matcha','Fresh matcha over ice',4.75,4]],
|
||||
'Food'=>[['Blueberry Muffin','Fresh-baked daily',3.00,1],['Avocado Toast','Multigrain with avocado & everything spice',7.50,2],['Breakfast Sandwich','Egg, cheese & choice of meat',6.50,3],['Cookie','Assorted fresh-baked cookies',2.50,4],['Granola Bar','Housemade granola bar',3.50,5]],
|
||||
'Teas'=>[['Earl Grey','Classic black tea with bergamot',3.00,1],['Green Tea','Organic loose-leaf green tea',3.00,2],['Chai Latte','Spiced tea with steamed milk',4.25,3],['Herbal Tea','Seasonal herbal blends',2.75,4]]]
|
||||
as $sec=>$items) foreach($items as $item) $mi->execute(array_merge([$qs[$sec]],$item));
|
||||
|
||||
// Candlewyck Inn
|
||||
$cwId = (int)qval("SELECT id FROM businesses WHERE slug='candlewyck-inn'");
|
||||
foreach ([['Starters','Elegant appetizers',1],['Soups & Salads','',2],['Entrées','Main courses',3],['Desserts','Sweet finishes',4]] as $s)
|
||||
$si->execute(array_merge([$cwId],$s));
|
||||
$cws=[]; foreach(qall("SELECT id,name FROM menu_sections WHERE business_id=$cwId") as $r) $cws[$r['name']]=$r['id'];
|
||||
foreach(['Starters'=>[['Crab Cakes','Pan-seared crab cakes, remoulade & microgreens',16.95,1],['Charcuterie Board','Artisan meats, local cheeses & seasonal accompaniments',21.95,2],['Escargot','Classic French preparation, garlic-herb butter & crusty bread',14.95,3]],
|
||||
'Soups & Salads'=>[['French Onion Soup','Rich beef broth, caramelized onions & gruyère crouton',10.95,1],['Wedge Salad','Iceberg, blue cheese, applewood bacon & cherry tomatoes',11.95,2]],
|
||||
'Entrées'=>[['8oz Filet Mignon','Center-cut tenderloin, béarnaise & pommes purée',48.95,1],['Rack of Lamb','Herb-crusted, minted demi-glace & root vegetable gratin',44.95,2],['Lobster Ravioli','House-made ravioli in cognac cream sauce',32.95,3],['Pan-Roasted Duck Breast','Cherry reduction, wild rice pilaf & roasted root vegetables',36.95,4]],
|
||||
'Desserts'=>[['Crème Brûlée','Classic vanilla bean custard with caramelized sugar',10.95,1],['Chocolate Fondant','Warm lava cake with vanilla ice cream',11.95,2],['Cheese Course','Three artisan cheeses, honeycomb, fruit & crackers',15.95,3]]]
|
||||
as $sec=>$items) foreach($items as $item) $mi->execute(array_merge([$cws[$sec]],$item));
|
||||
|
||||
/* ── ALERTS ─────────────────────────────────────────────────── */
|
||||
foreach ([
|
||||
['castiglia-italian','news','New Menu Items This Season!',
|
||||
"Chef's new handmade gnocchi and wood-fired seasonal pizza are now available. Fresh locally-sourced summer ingredients throughout the menu. Wednesday–Sunday, dine in and enjoy!"],
|
||||
['queens-point-coffee','news','Try Our Famous Iced Matcha!',
|
||||
'"THE best coffee shop I\'ve ever been to" — recent visitor. Our iced matcha with coconut and pistachio flavor is the most-talked-about drink in Keyser. Come see why!'],
|
||||
['clancys-irish-pub','event','Live Music Every Friday & Saturday!',
|
||||
"Live music from 9pm–1am every Friday and Saturday night. This Friday: Appalachian String Band. Saturday: Classic rock by Ridgeline. No cover charge! Food and drinks served until close."],
|
||||
['north-branch-craft-pub','event','Craft Beer Weekend — New Taps Arriving!',
|
||||
'New rotating craft beer taps arriving this Friday. Join us for our tap takeover event featuring 8 new local and regional brews. Food specials all weekend.'],
|
||||
['pvh','info','Urgent Care Now Open 7 Days',
|
||||
'WVU Medicine Potomac Valley Hospital Urgent Care is open Mon–Fri 8am–8pm and weekends 9am–5pm. No appointment needed for non-emergency care. ER open 24/7.'],
|
||||
['north-branch-ventures','event','Guided Kayak Tours — Book Now!',
|
||||
"Guided half-day kayak tours on the North Branch Potomac every Saturday and Sunday through October. All skill levels welcome — equipment and instruction included. Call 304-790-7125 to reserve."],
|
||||
] as [$slug,$type,$title,$body])
|
||||
qrun("INSERT INTO alerts(business_id,type,title,body,is_active)VALUES((SELECT id FROM businesses WHERE slug=?),?,?,?,1)",[$slug,$type,$title,$body]);
|
||||
|
||||
/* ── ATTRACTIONS ─────────────────────────────────────────────── */
|
||||
foreach ([
|
||||
['Queens Point Overlook','Natural Landmark',
|
||||
"A spectacular Oriskany sandstone cliff rising approximately 400 feet above the North Branch Potomac River in McCoole, MD — directly across from Keyser. Breathtaking panoramic views of Keyser and the river valley. One of Mineral County's most dramatic natural viewpoints and a beloved local landmark.",
|
||||
'McCoole, MD (viewable from Keyser riverfront)','','','Accessible year-round',1,1],
|
||||
['North Branch Potomac River','Outdoor Recreation',
|
||||
"The North Branch of the Potomac River runs along Keyser's northern edge, forming the WV/MD border. World-class smallmouth bass fishing, kayaking, canoeing, and bird watching. The river was instrumental in Mineral County's development. Contact North Branch Ventures for rentals and guided tours.",
|
||||
'Armstrong St. riverfront, Keyser, WV','304-790-7125','','Open year-round',1,2],
|
||||
['Larenim Park','County Park',
|
||||
"Mineral County's premier park at 365 acres. Two pavilions (10 picnic tables each), amphitheater seating 600, one Little League field and one softball field, two fishing ponds (5 and 2.5 acres stocked by WVDNR), approximately 5 miles of trails, and public hunting by permit. An arboretum is under construction. Home to McNeill's Rangers theater group.",
|
||||
'Keyser, WV 26726','304-788-3066','','Open daily, dawn to dusk',1,3],
|
||||
['Jennings Randolph Lake','Lake / Reservoir',
|
||||
"A stunning 952-acre reservoir on the North Branch Potomac, approximately 20 minutes from Keyser. Created by the Jennings Randolph Dam. Offers boating, fishing (walleye, bass, trout), swimming, camping, and picnicking. Also includes New Creek Trail — 4 miles of rail/trail with 1 mile of river frontage on the North Branch. Managed by U.S. Army Corps of Engineers.",
|
||||
'Elk Garden, WV 26717 (~20 min from Keyser)','304-355-2346','','Open year-round (camping seasonal)',1,4],
|
||||
['Mineral County Courthouse','Historic Site',
|
||||
"The iconic 1868 Mineral County Courthouse is listed on the National Register of Historic Places. This beautiful Victorian-era structure is the architectural centerpiece of downtown Keyser and a photographer's delight. The courthouse grounds are a popular community gathering spot year-round.",
|
||||
'Keyser, WV 26726','304-788-3003','','Exterior viewable anytime; interior Mon–Fri 8:30am–4:30pm',1,5],
|
||||
['Potomac State College Campus / Fort Fuller','Historic Site',
|
||||
"Fort Hill — site of Civil War Fort Fuller (also known as Fort Kelly) — was commanded by future U.S. President Benjamin Harrison and General Lew Wallace (who later wrote Ben-Hur). Today this historically significant ground is the beautiful campus of Potomac State College, part of WVU since 1901. A place where American presidential history meets higher education.",
|
||||
'101 Fort Ave., Keyser, WV 26726','304-788-6800','potomacstatecollege.edu','Campus open daily',1,6],
|
||||
['Nancy Hanks Birthplace Site','Historic Presidential Site',
|
||||
"The birthplace of Nancy Hanks — mother of President Abraham Lincoln — is preserved near Doll's Gap in Mineral County. This remarkable connection to America's most celebrated president is part of Mineral County's extraordinary heritage and is noted by West Virginia Explorer as a key county landmark.",
|
||||
"Near Doll's Gap, Mineral County, WV",'','','Viewable year-round',1,7],
|
||||
['Mineral County Historical Society Museum','Museum',
|
||||
"\"Recently opened, this museum tracks the history of Mineral County and Keyser, WV with displays of its people, its history, and its culture.\" Listed on TripAdvisor's top attractions in Keyser. Features exhibits on the B&O Railroad, Civil War, pioneers, and notable local figures. Open by appointment only and on special days.",
|
||||
'Keyser, WV 26726','304-788-3066','','By appointment; call before visiting',1,8],
|
||||
['Fort Ashby Blockhouse','Historic Site',
|
||||
"The original 1755 log blockhouse at Fort Ashby — the only remaining structure from the French and Indian War in West Virginia. Ordered built by a young George Washington as defense against frontier raids. A remarkable piece of American frontier military history just 15 miles from Keyser.",
|
||||
'Fort Ashby, WV 26719 (~15 miles from Keyser)','','','Open seasonally — contact WV Preservation',1,9],
|
||||
['Potomac Eagle Scenic Railroad','Scenic Train Excursion',
|
||||
"One of West Virginia's most beloved tourist attractions, just 30 miles from Keyser in Romney. The Potomac Eagle runs scenic excursions through The Trough along the South Branch Potomac River — famous for reliable bald eagle sightings. A must-do experience for families and nature lovers.",
|
||||
'Romney, WV 26757 (~30 miles from Keyser)','304-822-7400','potomaceagle.info','Seasonal spring through fall',1,10],
|
||||
['Blackwater Falls State Park','State Park',
|
||||
"One of WV's most iconic parks approximately 40 minutes from Keyser. The amber-colored Blackwater Falls plunge 57 feet into the dramatic Blackwater Canyon. Spectacular year-round views. Hiking, cross-country skiing, camping, and a beautiful lodge restaurant.",
|
||||
'Davis, WV 26260 (~40 min from Keyser)','304-259-5216','wvstateparks.com','Open year-round',1,11],
|
||||
['Seneca Rocks','Natural Landmark',
|
||||
"One of the most distinctive geological formations in the eastern U.S. — massive quartzite outcroppings rising 900 feet from the Seneca Creek valley floor. About 45 minutes from Keyser. World-class rock climbing, hiking, and breathtaking views. The Spruce Knob-Seneca Rocks National Recreation Area surrounds the formation.",
|
||||
'Seneca Rocks, WV (~45 min from Keyser)','304-567-2827','','Open year-round',1,12],
|
||||
['Canaan Valley Resort State Park','State Park / Ski Resort',
|
||||
"WV's highest valley at 3,200 ft elevation — largest inland wetland ecosystem in the eastern U.S. Skiing, snowboarding, hiking, mountain biking, golf, and stunning wildlife. About 50 minutes from Keyser. Beautiful lodge with restaurant.",
|
||||
'Davis, WV 26260 (~50 min from Keyser)','304-866-4121','canaanresort.com','Open year-round',1,13],
|
||||
['NedPower Mount Storm Wind Farm','Energy Landmark',
|
||||
"132 massive wind turbines atop the Allegheny Front, visible from Keyser on clear days. One of the largest wind energy installations east of the Mississippi River. The turbines have become part of Keyser's distinctive skyline and symbolize Appalachia's evolving energy economy.",
|
||||
'Mount Storm, WV (visible from Keyser)','','','Viewable from Keyser year-round',1,14],
|
||||
['Appalachian Forest National Heritage Area','Heritage Area',
|
||||
"Mineral County is part of the Appalachian Forest National Heritage Area — a forested region of more than 500 square miles in central Appalachia where tourism and forestry are key industries. Historic sites dating back to the Revolutionary War are found throughout the county, including stops on the B&O Railroad National Heritage Corridor.",
|
||||
'Keyser, WV (headquarters in Elkins)','','appalachianforestnha.org','Open year-round',1,15],
|
||||
] as $a) qrun("INSERT INTO attractions(name,category,description,address,phone,website,hours,is_active,sort_order)VALUES(?,?,?,?,?,?,?,?,?)",$a);
|
||||
|
||||
/* ── EVENTS ──────────────────────────────────────────────────── */
|
||||
$adminId = (int)qval("SELECT id FROM users WHERE username='administrator'");
|
||||
foreach ([
|
||||
['Mineral County Fair','Annual county fair featuring livestock shows, 4-H exhibits, carnival rides, food vendors, live entertainment, and a demolition derby. Sponsored by the Fort Ashby Volunteer Fire Co. One of Mineral County\'s biggest annual events.','Mineral County Fairgrounds','Fort Ashby, WV 26719','2025-08-12','9:00 AM','2025-08-16','10:00 PM','Admission varies','mineralcountyfair.org','','','304-788-0000',$adminId,'approved',1],
|
||||
['Keyser Christmas Parade & Tree Lighting','Annual holiday parade through downtown Keyser with decorated floats, marching bands, Santa Claus, and the official lighting of the downtown Christmas tree. A beloved community tradition — come join your neighbors!','Downtown Main Street','Keyser, WV 26726','2025-12-06','5:00 PM','2025-12-06','7:00 PM','Free','cityofkeyser.com','','','304-788-0222',$adminId,'approved',1],
|
||||
['PSC Catamount Athletics — Fall Season','Support the WVU Potomac State Catamounts at home athletic events throughout the fall semester. Football, basketball, soccer, baseball, and more. All events open to the public.','Potomac State College','101 Fort Ave., Keyser','2025-09-01','Varies','2025-11-30','Varies','Free–$5','potomacstatecollege.edu','','','304-788-6800',$adminId,'approved',0],
|
||||
['North Branch Kayak Weekend Tours','Guided and recreational kayaking on the North Branch Potomac River every Saturday and Sunday through October. All skill levels welcome. Equipment provided. Register through North Branch Ventures.','North Branch Ventures','101 Armstrong St., Keyser','2025-05-01','8:00 AM','2025-10-31','4:00 PM','$25–$45 per person','','','','304-790-7125',$adminId,'approved',0],
|
||||
['Mineral County Heritage Days','Community celebration of Mineral County history featuring historical reenactments, local artisan crafts, live traditional Appalachian music, food vendors, and family activities throughout downtown Keyser.','Downtown Keyser','Keyser, WV 26726','2025-10-11','10:00 AM','2025-10-12','5:00 PM','Free','govisitmineralwv.com','','','304-790-7081',$adminId,'approved',0],
|
||||
['PSC Cultural Arts Series','Performing arts events at Potomac State College open to the public — concerts, theatrical performances, guest lecturers, and gallery exhibitions throughout the academic year.','Potomac State College','101 Fort Ave., Keyser','2025-09-15','7:00 PM','2026-04-30','9:00 PM','Free–$10','potomacstatecollege.edu','','','304-788-6800',$adminId,'approved',0],
|
||||
['McNeill\'s Rangers Theater at Larenim Park','Outdoor theater performances by McNeill\'s Rangers at Larenim Park\'s 600-seat amphitheater. Classic American stories brought to life in a beautiful natural setting.','Larenim Park Amphitheater','Keyser, WV 26726','2025-07-18','7:30 PM','2025-07-19','10:00 PM','$10–$20','','','','304-788-3066',$adminId,'approved',0],
|
||||
] as $ev) qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,submitted_by,status,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",$ev);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/* ── Auth ─────────────────────────────────────────────── */
|
||||
function uid(): int { return (int)($_SESSION['uid'] ?? 0); }
|
||||
function uname(): string{ return (string)($_SESSION['uname'] ?? ''); }
|
||||
function authed(): bool { return uid() > 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 '<input type="hidden" name="_csrf" value="'.e(csrf()).'">'; }
|
||||
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 ? (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 = round($avg); $out = '<span class="stars">';
|
||||
for ($i = 1; $i <= 5; $i++) $out .= '<span class="star'.($i<=$r?' on':'').'">★</span>';
|
||||
return $out.'</span>';
|
||||
}
|
||||
function starPicker(string $name = 'rating', int $sel = 0): string {
|
||||
$out = '<div class="star-picker">';
|
||||
for ($i = 5; $i >= 1; $i--) {
|
||||
$chk = $sel === $i ? 'checked' : '';
|
||||
$out .= '<input type="radio" id="sr'.$i.'" name="'.$name.'" value="'.$i.'" '.$chk.'>';
|
||||
$out .= '<label for="sr'.$i.'">★</label>';
|
||||
}
|
||||
return $out.'</div>';
|
||||
}
|
||||
|
||||
/* ── Date / Money ─────────────────────────────────────── */
|
||||
function ago(string $dt): string {
|
||||
$d = time() - strtotime($dt);
|
||||
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', strtotime($dt));
|
||||
}
|
||||
function fdate(string $dt, string $fmt = 'M j, Y'): string { return date($fmt, strtotime($dt)); }
|
||||
function money(float $n): string { return '$'.number_format($n, 2); }
|
||||
|
||||
/* ── Misc ─────────────────────────────────────────────── */
|
||||
function alertIcon(string $t): string { return match($t){'news'=>'📰','event'=>'🎉','warning'=>'⚠️','sale'=>'🏷️',default=>'ℹ️'}; }
|
||||
function parseHours(string $json): array { return json_decode($json ?: '{}', true) ?? []; }
|
||||
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" : "");
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/* ── Auth ─────────────────────────────────────────────── */
|
||||
function uid(): int { return (int)($_SESSION['uid'] ?? 0); }
|
||||
function uname(): string{ return (string)($_SESSION['uname'] ?? ''); }
|
||||
function authed(): bool { return uid() > 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 '<input type="hidden" name="_csrf" value="'.e(csrf()).'">'; }
|
||||
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 ? (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 = round($avg); $out = '<span class="stars">';
|
||||
for ($i = 1; $i <= 5; $i++) $out .= '<span class="star'.($i<=$r?' on':'').'">★</span>';
|
||||
return $out.'</span>';
|
||||
}
|
||||
function starPicker(string $name = 'rating', int $sel = 0): string {
|
||||
$out = '<div class="star-picker">';
|
||||
for ($i = 5; $i >= 1; $i--) {
|
||||
$chk = $sel === $i ? 'checked' : '';
|
||||
$out .= '<input type="radio" id="sr'.$i.'" name="'.$name.'" value="'.$i.'" '.$chk.'>';
|
||||
$out .= '<label for="sr'.$i.'">★</label>';
|
||||
}
|
||||
return $out.'</div>';
|
||||
}
|
||||
|
||||
/* ── Date / Money ─────────────────────────────────────── */
|
||||
function ago(string $dt): string {
|
||||
$d = time() - strtotime($dt);
|
||||
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', strtotime($dt));
|
||||
}
|
||||
function fdate(string $dt, string $fmt = 'M j, Y'): string { return date($fmt, strtotime($dt)); }
|
||||
function money(float $n): string { return '$'.number_format($n, 2); }
|
||||
|
||||
/* ── Misc ─────────────────────────────────────────────── */
|
||||
function alertIcon(string $t): string { return match($t){'news'=>'📰','event'=>'🎉','warning'=>'⚠️','sale'=>'🏷️',default=>'ℹ️'}; }
|
||||
function parseHours(?string $json): array { return json_decode(($json && $json !== '[]') ? $json : '{}', true) ?? []; }
|
||||
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" : "");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
require_once __DIR__.'/db.php';
|
||||
require_once __DIR__.'/functions.php';
|
||||
db();
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
<?php
|
||||
/**
|
||||
* Keyser WV Tourism Site — Database Layer
|
||||
* SQLite via PDO; DB lives in /data/ (chmod 777)
|
||||
*/
|
||||
define('DATA_DIR', dirname(__DIR__).'/data');
|
||||
define('DB_FILE', DATA_DIR.'/keyser.db');
|
||||
|
||||
function db(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
if (!is_dir(DATA_DIR)) mkdir(DATA_DIR, 0777, true);
|
||||
@chmod(DATA_DIR, 0777);
|
||||
$isNew = !file_exists(DB_FILE) || filesize(DB_FILE) === 0;
|
||||
$pdo = new PDO('sqlite:'.DB_FILE, null, null, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$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);
|
||||
if (file_exists(DB_FILE)) @chmod(DB_FILE, 0666);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/* ── Shorthand query helpers ────────────────────────────── */
|
||||
function qr(string $sql, array $p=[]): PDOStatement { $s=db()->prepare($sql); $s->execute($p); return $s; }
|
||||
function qall(string $sql, array $p=[]): array { return qr($sql,$p)->fetchAll(); }
|
||||
function qone(string $sql, array $p=[]): ?array { $r=qr($sql,$p)->fetch(); return $r?:null; }
|
||||
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 {
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS business_edit_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,
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT NOT NULL DEFAULT '',
|
||||
new_value 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
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
SCHEMA
|
||||
══════════════════════════════════════════════════════ */
|
||||
function _schema(PDO $db): void {
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS settings(
|
||||
key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT,
|
||||
password TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
last_login TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS categories(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🏢',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS businesses(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
category_id INTEGER REFERENCES categories(id),
|
||||
subcategory TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
website TEXT NOT NULL DEFAULT '',
|
||||
hours TEXT NOT NULL DEFAULT '{}',
|
||||
lat REAL NOT NULL DEFAULT 39.4364,
|
||||
lng REAL NOT NULL DEFAULT -78.9762,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
is_featured INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS business_owners(
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(user_id,business_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS business_edit_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,
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT NOT NULL DEFAULT '',
|
||||
new_value 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,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
is_approved INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS menu_sections(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS menu_items(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
section_id INTEGER NOT NULL REFERENCES menu_sections(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
price REAL NOT NULL DEFAULT 0,
|
||||
is_available INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS alerts(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL DEFAULT 'info',
|
||||
title TEXT NOT NULL,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS attractions(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'Sightseeing',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
website TEXT NOT NULL DEFAULT '',
|
||||
hours TEXT NOT NULL DEFAULT '',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS events(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
venue TEXT NOT NULL DEFAULT '',
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
event_date TEXT NOT NULL,
|
||||
event_time TEXT NOT NULL DEFAULT '',
|
||||
end_date TEXT,
|
||||
end_time TEXT NOT NULL DEFAULT '',
|
||||
cost TEXT NOT NULL DEFAULT 'Free',
|
||||
website TEXT NOT NULL DEFAULT '',
|
||||
contact_name TEXT NOT NULL DEFAULT '',
|
||||
contact_email TEXT NOT NULL DEFAULT '',
|
||||
contact_phone TEXT NOT NULL DEFAULT '',
|
||||
submitted_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
is_featured INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS reports(
|
||||
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,
|
||||
type TEXT NOT NULL DEFAULT 'correction',
|
||||
message TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
admin_notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
SEED DATA
|
||||
══════════════════════════════════════════════════════ */
|
||||
function _seed(PDO $db): void {
|
||||
|
||||
/* Settings */
|
||||
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)
|
||||
qrun("INSERT OR IGNORE INTO settings(key,value)VALUES(?,?)",[$k,$v]);
|
||||
|
||||
/* Admin user — password "password123" */
|
||||
$hash = password_hash('password123', PASSWORD_BCRYPT);
|
||||
qrun("INSERT OR IGNORE INTO users(username,email,password,is_admin)
|
||||
VALUES('administrator','admin@discoverkeyser.com',?,1)",[$hash]);
|
||||
|
||||
/* Categories */
|
||||
foreach ([
|
||||
['Dining & Food','🍽️',1],['Shopping & Retail','🛍️',2],
|
||||
['Lodging','🏨',3],['Healthcare','🏥',4],['Education','🎓',5],
|
||||
['Financial Services','🏦',6],['Automotive','🔧',7],
|
||||
['Professional Services','💼',8],['Recreation & Fitness','⛹️',9],
|
||||
['Beauty & Personal Care','💇',10],['Government & Civic','🏛️',11],
|
||||
['Legal Services','⚖️',12],['Churches & Faith','⛪',13],
|
||||
] as $c) qrun("INSERT OR IGNORE INTO categories(name,icon,sort_order)VALUES(?,?,?)",$c);
|
||||
|
||||
$cid = fn(string $n): int => (int)qval("SELECT id FROM categories WHERE name=?",[$n]);
|
||||
$h = fn(array $d): string => json_encode($d);
|
||||
$std = $h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'Closed','Sun'=>'Closed']);
|
||||
$always= $h(['Mon'=>'Open 24 hrs','Tue'=>'Open 24 hrs','Wed'=>'Open 24 hrs','Thu'=>'Open 24 hrs','Fri'=>'Open 24 hrs','Sat'=>'Open 24 hrs','Sun'=>'Open 24 hrs']);
|
||||
|
||||
$D=$cid('Dining & Food'); $SH=$cid('Shopping & Retail'); $LO=$cid('Lodging');
|
||||
$HC=$cid('Healthcare'); $ED=$cid('Education'); $FI=$cid('Financial Services');
|
||||
$AU=$cid('Automotive'); $PS=$cid('Professional Services'); $RF=$cid('Recreation & Fitness');
|
||||
$BP=$cid('Beauty & Personal Care'); $GV=$cid('Government & Civic');
|
||||
$LG=$cid('Legal Services'); $CH=$cid('Churches & Faith');
|
||||
|
||||
$bi = $db->prepare("INSERT INTO businesses(name,slug,category_id,subcategory,description,address,phone,email,website,hours,lat,lng,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
|
||||
$biz = [
|
||||
["Castiglia's Italian Eatery",'castiglia-italian',$D,'Italian Restaurant',
|
||||
"Keyser's most beloved restaurant. Family recipes passed down for generations: handmade pasta, wood-fired dishes, classic antipasti, and decadent Italian desserts. Known for huge servings and amazing food.",
|
||||
'401 S. Mineral St., Keyser, WV 26726','304-788-1300','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–9pm','Sat'=>'11am–9pm','Sun'=>'12pm–9pm']),39.4340,-78.9734,1],
|
||||
['The Candlewyck Inn','candlewyck-inn',$D,'Fine Dining',
|
||||
"Elegant fine dining in a beautifully restored historic setting. Upscale American cuisine with locally sourced seasonal ingredients. Award-winning wine list. Keyser's premier destination for special occasions.",
|
||||
'65 S. Mineral St., Keyser, WV 26726','304-788-6594','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'5pm–9pm','Wed'=>'5pm–9pm','Thu'=>'5pm–9pm','Fri'=>'5pm–10pm','Sat'=>'4pm–10pm','Sun'=>'Closed']),39.4370,-78.9734,1],
|
||||
["Clancy's Irish Pub",'clancys-irish-pub',$D,'Irish Pub & Bar',
|
||||
"Keyser's favorite neighborhood pub! Cold craft beers on tap, hearty pub fare, sports on every screen, trivia nights on Wednesdays, and live music on weekends.",
|
||||
'777 Armstrong St., Keyser, WV 26726','304-788-1133','','',
|
||||
$h(['Mon'=>'4pm–12am','Tue'=>'4pm–12am','Wed'=>'4pm–12am','Thu'=>'4pm–12am','Fri'=>'4pm–2am','Sat'=>'12pm–2am','Sun'=>'12pm–10pm']),39.4390,-78.9750,1],
|
||||
['North Branch Craft Pub','north-branch-craft-pub',$D,'Craft Brewery & Pub',
|
||||
"Keyser's craft beer destination. Local and regional craft beers on rotating taps, great pub fare, and stunning North Branch river views. Live entertainment regularly.",
|
||||
'101 Armstrong St., Ste. 1, Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'4pm–11pm','Wed'=>'4pm–11pm','Thu'=>'4pm–11pm','Fri'=>'3pm–1am','Sat'=>'12pm–1am','Sun'=>'12pm–8pm']),39.4392,-78.9750,1],
|
||||
['Queens Point Coffee','queens-point-coffee',$D,'Coffee Shop',
|
||||
'"THE best coffee shop I\'ve ever been to." Named after the iconic Queens Point cliff. Iced matcha, lattes, specialty drinks, amazing food, and friendly staff.',
|
||||
'101 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'7am–3pm','Tue'=>'7am–5pm','Wed'=>'7am–5pm','Thu'=>'7am–5pm','Fri'=>'7am–5pm','Sat'=>'8am–5pm','Sun'=>'9am–2pm']),39.4392,-78.9750,1],
|
||||
['Royal Restaurant','royal-restaurant',$D,'American / Comfort Food',
|
||||
"A Keyser classic for generations. Hearty American comfort food for breakfast, lunch, and dinner. Known for great breakfasts, homemade pies, and friendly small-town service.",
|
||||
'88 N. Main St., Keyser, WV 26726','304-788-9825','','',
|
||||
$h(['Mon'=>'6am–8pm','Tue'=>'6am–8pm','Wed'=>'6am–8pm','Thu'=>'6am–8pm','Fri'=>'6am–9pm','Sat'=>'6am–9pm','Sun'=>'7am–3pm']),39.4378,-78.9734,0],
|
||||
['Fat Bottom Grille','fat-bottom-grille',$D,'American Bar & Grill',
|
||||
'"THE best place in town to get a burger or steak sub." Everything made fresh. The smash burger is juicy and flavored to perfection. Huge portions at excellent prices.',
|
||||
'Keyser, WV 26726','304-788-0000','','',
|
||||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),39.4360,-78.9740,1],
|
||||
["Denny's",'dennys',$D,'American / Diner',
|
||||
"Open 24/7. America's diner at Route 220 South. Grand Slams, hearty burgers, and all-day breakfast.",
|
||||
'825 S. Mineral St. (US Rt. 220 S), Keyser, WV 26726','304-788-3090','','dennys.com',$always,39.4300,-78.9730,0],
|
||||
["McDonald's",'mcdonalds',$D,'Fast Food',
|
||||
"Burgers, fries, breakfast sandwiches, and McCafé beverages. Drive-through available.",
|
||||
'700 S. Mineral St., Keyser, WV 26726','304-788-0604','','mcdonalds.com',
|
||||
$h(['Mon'=>'6am–11pm','Tue'=>'6am–11pm','Wed'=>'6am–11pm','Thu'=>'6am–11pm','Fri'=>'6am–12am','Sat'=>'6am–12am','Sun'=>'7am–11pm']),39.4310,-78.9730,0],
|
||||
['Burger King','burger-king',$D,'Fast Food',
|
||||
'Home of the Whopper! Flame-grilled burgers and crispy fries. Drive-through available.',
|
||||
'RR#3 Box 3240, New Creek Hwy, Keyser, WV 26726','304-788-0000','','burgerking.com',
|
||||
$h(['Mon'=>'6am–12am','Tue'=>'6am–12am','Wed'=>'6am–12am','Thu'=>'6am–12am','Fri'=>'6am–1am','Sat'=>'6am–1am','Sun'=>'7am–12am']),39.4285,-78.9720,0],
|
||||
["Domino's Pizza",'dominos',$D,'Pizza / Delivery',
|
||||
'Hot fresh pizza fast. Carryout or delivered. Online ordering at dominos.com.',
|
||||
'590 S. Mineral St., Keyser, WV 26726','304-788-6400','','dominos.com',
|
||||
$h(['Mon'=>'10:30am–12am','Tue'=>'10:30am–12am','Wed'=>'10:30am–12am','Thu'=>'10:30am–12am','Fri'=>'10:30am–1am','Sat'=>'10:30am–1am','Sun'=>'10:30am–12am']),39.4330,-78.9730,0],
|
||||
['Dairy Queen Grill & Chill','dairy-queen',$D,'Ice Cream / Fast Food',
|
||||
"Signature Blizzard® treats, soft-serve cones, and grill burgers. A Keyser summer tradition!",
|
||||
'460 S. Mineral St., Keyser, WV 26726','304-788-1499','','dairyqueen.com',
|
||||
$h(['Mon'=>'10am–10pm','Tue'=>'10am–10pm','Wed'=>'10am–10pm','Thu'=>'10am–10pm','Fri'=>'10am–10:30pm','Sat'=>'10am–10:30pm','Sun'=>'11am–9pm']),39.4320,-78.9730,0],
|
||||
["Fox's Pizza Den",'foxs-pizza',$D,'Pizza',
|
||||
"Local pizza legend! Hand-tossed pizzas, subs, wings, and salads. A true Keyser staple.",
|
||||
'567 S. Mineral St., Keyser, WV 26726','304-788-1149','','',
|
||||
$h(['Mon'=>'11am–10pm','Tue'=>'11am–10pm','Wed'=>'11am–10pm','Thu'=>'11am–10pm','Fri'=>'11am–11pm','Sat'=>'11am–11pm','Sun'=>'12pm–9pm']),39.4315,-78.9730,0],
|
||||
['Little Caesars','little-caesars',$D,'Pizza / Fast Food',
|
||||
'Hot-N-Ready pizzas at unbeatable prices. No wait. Crazy Bread and Italian favorites.',
|
||||
'30 Armstrong St., Keyser, WV 26726','304-788-7738','','littlecaesars.com',
|
||||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),39.4395,-78.9750,0],
|
||||
['Subway','subway',$D,'Sandwiches / Fast Food',
|
||||
'Fresh-built footlong subs, wraps, and salads.',
|
||||
'2 Heskiet St. (Gulf), Keyser, WV 26726','304-788-3613','','subway.com',
|
||||
$h(['Mon'=>'7am–10pm','Tue'=>'7am–10pm','Wed'=>'7am–10pm','Thu'=>'7am–10pm','Fri'=>'7am–10pm','Sat'=>'8am–10pm','Sun'=>'9am–9pm']),39.4260,-78.9720,0],
|
||||
['Taco Bell','taco-bell',$D,'Mexican Fast Food',
|
||||
'Tacos, burritos, nachos, and Crunchwrap Supremes. Drive-through available.',
|
||||
'41 Plaza Drive, Keyser, WV 26726','304-788-0000','','tacobell.com',
|
||||
$h(['Mon'=>'7am–12am','Tue'=>'7am–12am','Wed'=>'7am–12am','Thu'=>'7am–12am','Fri'=>'7am–2am','Sat'=>'7am–2am','Sun'=>'8am–12am']),39.4265,-78.9722,0],
|
||||
["Ducky's Bar & Grill",'duckys-bar-grill',$D,'American Bar & Grill',
|
||||
'"The ultimate combination of bar and grill." Famous Tennessee whiskey burger and chili cheese fries.',
|
||||
'Keyser, WV 26726','304-788-0000','','',
|
||||
$h(['Mon'=>'11am–11pm','Tue'=>'11am–11pm','Wed'=>'11am–11pm','Thu'=>'11am–11pm','Fri'=>'11am–1am','Sat'=>'11am–1am','Sun'=>'12pm–9pm']),39.4358,-78.9738,0],
|
||||
["Martie's Hot Dog Stand",'marties-hot-dogs',$D,'Hot Dogs / Street Food',
|
||||
"A beloved Keyser institution. Classic hot dogs with all the fixings for generations.",
|
||||
'96 N. Main St., Keyser, WV 26726','304-788-7690','','',
|
||||
$h(['Mon'=>'10am–6pm','Tue'=>'10am–6pm','Wed'=>'10am–6pm','Thu'=>'10am–6pm','Fri'=>'10am–7pm','Sat'=>'10am–7pm','Sun'=>'Closed']),39.4375,-78.9734,0],
|
||||
["Jin's Asian Cuisine 2",'jins-asian-cuisine',$D,'Asian / Chinese',
|
||||
"Orange chicken, bourbon chicken, lo mein, fried rice, and crab rangoon made fresh to order.",
|
||||
'196 N. Tornado Way #11, Keyser, WV 26726','304-788-2222','','',
|
||||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),39.4355,-78.9730,0],
|
||||
// Shopping
|
||||
["Wayne's Country Meats",'waynes-country-meats',$SH,'Butcher / Specialty Meats',
|
||||
'Premium quality meats from local farms. Custom cuts, specialty items, fresh jerky.',
|
||||
'670 Armstrong St., Keyser, WV 26726','304-788-5956','','',
|
||||
$h(['Mon'=>'8am–6pm','Tue'=>'8am–6pm','Wed'=>'8am–6pm','Thu'=>'8am–6pm','Fri'=>'8am–6pm','Sat'=>'8am–5pm','Sun'=>'Closed']),39.4395,-78.9750,1],
|
||||
['Save-A-Lot','save-a-lot',$SH,'Grocery',
|
||||
'Affordable grocery shopping. Fresh produce, quality meats, dairy, and pantry essentials.',
|
||||
'S. Mineral St., Keyser, WV 26726','304-788-7570','','save-a-lot.com',
|
||||
$h(['Mon'=>'8am–9pm','Tue'=>'8am–9pm','Wed'=>'8am–9pm','Thu'=>'8am–9pm','Fri'=>'8am–9pm','Sat'=>'8am–9pm','Sun'=>'9am–7pm']),39.4298,-78.9728,0],
|
||||
['AutoZone','autozone',$SH,'Auto Parts',
|
||||
"America's leading auto parts retailer. Free battery testing, loaner tool program.",
|
||||
'2 W. Piedmont St., Keyser, WV 26726','304-788-9058','','autozone.com',
|
||||
$h(['Mon'=>'7:30am–9pm','Tue'=>'7:30am–9pm','Wed'=>'7:30am–9pm','Thu'=>'7:30am–9pm','Fri'=>'7:30am–9pm','Sat'=>'7:30am–9pm','Sun'=>'9am–8pm']),39.4378,-78.9734,0],
|
||||
['CVS Pharmacy','cvs',$SH,'Pharmacy / Health & Beauty',
|
||||
'Prescriptions, health products, beauty supplies, and household essentials.',
|
||||
'45 S. Mineral St., Keyser, WV 26726','304-788-3443','','cvs.com',
|
||||
$h(['Mon'=>'8am–9pm','Tue'=>'8am–9pm','Wed'=>'8am–9pm','Thu'=>'8am–9pm','Fri'=>'8am–9pm','Sat'=>'9am–6pm','Sun'=>'10am–6pm']),39.4368,-78.9734,0],
|
||||
['AT&T Store','att-store',$SH,'Telecommunications',
|
||||
'Latest smartphones, tablets, and wireless plans.',
|
||||
'862 S. Mineral St., Keyser, WV 26726','304-788-9000','','att.com',
|
||||
$h(['Mon'=>'10am–7pm','Tue'=>'10am–7pm','Wed'=>'10am–7pm','Thu'=>'10am–7pm','Fri'=>'10am–7pm','Sat'=>'10am–7pm','Sun'=>'12pm–5pm']),39.4292,-78.9728,0],
|
||||
["Christy's Florist",'christys-florist',$SH,'Florist',
|
||||
'Fresh floral arrangements for weddings, funerals, anniversaries, and celebrations.',
|
||||
'81 N. Main St., Keyser, WV 26726','304-788-3679','','',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'9am–3pm','Sun'=>'Closed']),39.4382,-78.9734,0],
|
||||
['Southern States','southern-states',$SH,'Farm & Garden Supply',
|
||||
'Agricultural supplies, pet food, hardware, seed, fertilizer, and home and garden products.',
|
||||
'201 Patrick St., Keyser, WV 26726','304-788-2317','','southernstates.com',
|
||||
$h(['Mon'=>'7:30am–5:30pm','Tue'=>'7:30am–5:30pm','Wed'=>'7:30am–5:30pm','Thu'=>'7:30am–5:30pm','Fri'=>'7:30am–5:30pm','Sat'=>'8am–3pm','Sun'=>'Closed']),39.4410,-78.9755,0],
|
||||
['Back Alley Crafts & Tarts','back-alley-crafts',$SH,'Crafts & Gifts',
|
||||
'Unique handmade crafts, locally made artisan goods, and specialty gifts.',
|
||||
'226 S. Main St., Keyser, WV 26726','304-813-6256','','',
|
||||
$h(['Mon'=>'10am–5pm','Tue'=>'10am–5pm','Wed'=>'10am–5pm','Thu'=>'10am–5pm','Fri'=>'10am–6pm','Sat'=>'10am–4pm','Sun'=>'Closed']),39.4357,-78.9734,0],
|
||||
['Heaven Sent Creations','heaven-sent-creations',$SH,'Gifts & Crafts',
|
||||
'Inspirational gifts, custom creations, home décor, and unique finds.',
|
||||
'117 Armstrong St., Keyser, WV 26726','304-790-7402','','',
|
||||
$h(['Mon'=>'10am–5pm','Tue'=>'10am–5pm','Wed'=>'10am–5pm','Thu'=>'10am–5pm','Fri'=>'10am–5pm','Sat'=>'10am–4pm','Sun'=>'Closed']),39.4393,-78.9750,0],
|
||||
['Thunder Hill Outfitters','thunder-hill-outfitters',$SH,'Hunting & Fishing Supplies',
|
||||
"Hunting and fishing supplies, gear, licenses, and local expertise.",
|
||||
'105 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–7pm','Sat'=>'8am–7pm','Sun'=>'10am–4pm']),39.4392,-78.9750,0],
|
||||
// Lodging
|
||||
['SureStay Plus by Best Western Keyser','surestay-best-western',$LO,'Hotel',
|
||||
'Free hot breakfast daily, free WiFi, indoor pool, and fitness center. Pet-friendly.',
|
||||
'New Creek Hwy, Keyser, WV 26726','304-788-0000','','bestwestern.com',$always,39.4298,-78.9705,1],
|
||||
['Keyser Inn','keyser-inn',$LO,'Motel',
|
||||
'Affordable, clean accommodations. Continental breakfast, free WiFi, pet-friendly.',
|
||||
'Keyser, WV 26726','304-788-0000','','',$always,39.4360,-78.9734,0],
|
||||
// Healthcare
|
||||
['WVU Medicine Potomac Valley Hospital','pvh',$HC,'Hospital / Medical Center',
|
||||
'Full-service community hospital. Emergency department, surgery, maternity care, cardiac services.',
|
||||
'100 Pin Oak Ln., Keyser, WV 26726','304-597-3500','','wvumedicine.org',$always,39.4275,-78.9725,1],
|
||||
['Med-a-Save Pharmacy','med-a-save',$HC,'Independent Pharmacy',
|
||||
'Local independent pharmacy. Prescriptions, compounding, and immunizations.',
|
||||
'818 S. Mineral St., Keyser, WV 26726','304-788-6010','','',
|
||||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–6pm','Sat'=>'9am–1pm','Sun'=>'Closed']),39.4287,-78.9728,0],
|
||||
// Education
|
||||
['Potomac State College of WVU','potomac-state-college',$ED,'Community College / University',
|
||||
"WVU Potomac State College — founded 1901 on historic Fort Fuller. Associate and bachelor's degrees in 40+ programs. Home of the Catamounts.",
|
||||
'101 Fort Ave., Keyser, WV 26726','304-788-6800','','potomacstatecollege.edu',$std,39.4368,-78.9715,1],
|
||||
['Mineral County Public Library','mineral-county-library',$ED,'Public Library',
|
||||
'Books, digital resources, internet access, printing, and community programs for all ages.',
|
||||
'Keyser, WV 26726','304-788-3100','','',
|
||||
$h(['Mon'=>'10am–7pm','Tue'=>'10am–7pm','Wed'=>'10am–7pm','Thu'=>'10am–7pm','Fri'=>'10am–5pm','Sat'=>'10am–4pm','Sun'=>'Closed']),39.4370,-78.9734,0],
|
||||
// Financial
|
||||
["First United Bank & Trust",'first-united-bank',$FI,'Community Bank',
|
||||
'Community banking rooted in West Virginia. Personal checking, savings, mortgages.',
|
||||
'29 West Southern Dr., Keyser, WV 26726','304-788-2552','','mybank.com',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5:30pm','Sat'=>'9am–12pm','Sun'=>'Closed']),39.4372,-78.9740,0],
|
||||
['M&T Bank','m-t-bank',$FI,'Bank',
|
||||
'Full-service personal, small business, and commercial banking. 24/7 ATM.',
|
||||
'67 N. Main St., Keyser, WV 26726','304-788-6782','','mtb.com',
|
||||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'9am–12pm','Sun'=>'Closed']),39.4380,-78.9734,0],
|
||||
// Automotive
|
||||
["Boddy's Automotive",'boddys-auto',$AU,'Full Service Auto Repair',
|
||||
'Trusted local auto repair for all makes and models. Honest prices.',
|
||||
'220 Armstrong St., Keyser, WV 26726','304-788-5511','','',
|
||||
$h(['Mon'=>'8am–5pm','Tue'=>'8am–5pm','Wed'=>'8am–5pm','Thu'=>'8am–5pm','Fri'=>'8am–5pm','Sat'=>'Closed','Sun'=>'Closed']),39.4392,-78.9750,0],
|
||||
['Gimme A Brake Auto','gimme-a-brake',$AU,'Brakes / Tires / Oil Changes',
|
||||
'Fast, friendly, and affordable auto care. No appointment needed for routine maintenance.',
|
||||
'375 West Piedmont St., Keyser, WV 26726','304-788-7810','','',
|
||||
$h(['Mon'=>'8am–5:30pm','Tue'=>'8am–5:30pm','Wed'=>'8am–5:30pm','Thu'=>'8am–5:30pm','Fri'=>'8am–5:30pm','Sat'=>'8am–12pm','Sun'=>'Closed']),39.4378,-78.9748,0],
|
||||
// Professional Services
|
||||
['Boggs Supply and Rental Center','boggs-supply',$PS,'Equipment Rental / Supply',
|
||||
'Tools, equipment, and supplies for rent or purchase.',
|
||||
'464 Harley O Staggers Sr. Dr., Keyser, WV 26726','304-788-1617','','',
|
||||
$h(['Mon'=>'7:30am–5pm','Tue'=>'7:30am–5pm','Wed'=>'7:30am–5pm','Thu'=>'7:30am–5pm','Fri'=>'7:30am–5pm','Sat'=>'8am–12pm','Sun'=>'Closed']),39.4382,-78.9748,0],
|
||||
['Legal Aid of WV','legal-aid',$LG,'Legal Aid',
|
||||
'Free civil legal aid for eligible low-income West Virginians.',
|
||||
'251 W. Piedmont St., Keyser, WV 26726','304-788-6770','','lawv.net',$std,39.4374,-78.9750,0],
|
||||
// Recreation
|
||||
['North Branch Ventures','north-branch-ventures',$RF,'Kayak Rental / Outdoor Recreation',
|
||||
"Kayak and river supply rentals on the beautiful North Branch Potomac. Guided river experiences.",
|
||||
'101 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'10am–6pm','Thu'=>'10am–6pm','Fri'=>'9am–7pm','Sat'=>'8am–7pm','Sun'=>'8am–5pm']),39.4392,-78.9750,1],
|
||||
['PSC Recreation Center','psc-rec-center',$RF,'Fitness Center / Pool',
|
||||
"Full-service fitness center with indoor pool, hot tub, strength and cardio, group exercise. Open to the public.",
|
||||
'J. Edward Kelley Complex, 101 Fort Ave., Keyser, WV','304-788-6800','','potomacstatecollege.edu',
|
||||
$h(['Mon'=>'6am–9pm','Tue'=>'6am–9pm','Wed'=>'6am–9pm','Thu'=>'6am–9pm','Fri'=>'6am–8pm','Sat'=>'8am–6pm','Sun'=>'12pm–6pm']),39.4368,-78.9715,0],
|
||||
['Mineral County Parks & Recreation','mineral-county-parks',$RF,'Parks & Recreation',
|
||||
"Larenim Park — 365 acres with pavilions, 600-seat amphitheater, fishing ponds, and trails.",
|
||||
'Keyser, WV 26726','304-788-3066','','mineralwv.gov',
|
||||
$h(['Mon'=>'Open Daily','Tue'=>'Open Daily','Wed'=>'Open Daily','Thu'=>'Open Daily','Fri'=>'Open Daily','Sat'=>'Open Daily','Sun'=>'Open Daily']),39.4350,-78.9720,0],
|
||||
// Beauty
|
||||
['Eclips Hair Salon','eclips',$BP,'Hair Salon',
|
||||
'Full-service hair salon. Cuts, color, styling, and treatments for men and women.',
|
||||
'153 S. Mineral St., Keyser, WV 26726','304-788-5578','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'8am–4pm','Sun'=>'Closed']),39.4352,-78.9733,0],
|
||||
['In The Skin II','in-the-skin',$BP,'Tattoo & Body Art',
|
||||
'Professional tattoo and body art studio. Custom designs, cover-ups, and piercings.',
|
||||
'129 N. Main St., Keyser, WV 26726','304-597-2070','','',
|
||||
$h(['Mon'=>'11am–7pm','Tue'=>'11am–7pm','Wed'=>'11am–7pm','Thu'=>'11am–7pm','Fri'=>'11am–8pm','Sat'=>'10am–6pm','Sun'=>'Closed']),39.4373,-78.9734,0],
|
||||
// Government
|
||||
['Mineral County Courthouse','mineral-county-courthouse',$GV,'County Government',
|
||||
"Historic 1868 courthouse listed on the National Register of Historic Places — the architectural crown jewel of downtown Keyser.",
|
||||
'Keyser, WV 26726','304-788-3003','','mineralwv.gov',
|
||||
$h(['Mon'=>'8:30am–4:30pm','Tue'=>'8:30am–4:30pm','Wed'=>'8:30am–4:30pm','Thu'=>'8:30am–4:30pm','Fri'=>'8:30am–4:30pm','Sat'=>'Closed','Sun'=>'Closed']),39.4385,-78.9738,0],
|
||||
['City of Keyser','city-of-keyser',$GV,'City Government',
|
||||
"Official offices of the City of Keyser, WV. City council, mayor's office, utilities, and municipal services.",
|
||||
'Keyser, WV 26726','304-788-0222','','cityofkeyser.com',
|
||||
$h(['Mon'=>'8:30am–4:30pm','Tue'=>'8:30am–4:30pm','Wed'=>'8:30am–4:30pm','Thu'=>'8:30am–4:30pm','Fri'=>'8:30am–4:30pm','Sat'=>'Closed','Sun'=>'Closed']),39.4375,-78.9734,0],
|
||||
['Mineral County Tourism Visitors Center','mineral-county-tourism',$GV,'Tourism / Visitors Center',
|
||||
'167 S. Mineral Street — Your starting point for exploring Mineral County! Brochures, maps, and friendly staff.',
|
||||
'167 S. Mineral St., Keyser, WV 26726','304-790-7081','mineralcocvb@gmail.com','govisitmineralwv.com',
|
||||
$h(['Mon'=>'9am–4pm','Tue'=>'9am–4pm','Wed'=>'9am–4pm','Thu'=>'9am–4pm','Fri'=>'9am–4pm','Sat'=>'Call Ahead','Sun'=>'Closed']),39.4358,-78.9734,1],
|
||||
['Mineral County Historical Society Museum','mineral-county-historical',$GV,'Museum / Historical Society',
|
||||
"Museum tracking the history of Mineral County and Keyser with engaging exhibits and artifacts. Open by appointment.",
|
||||
'Keyser, WV 26726','304-788-3066','','',
|
||||
$h(['Mon'=>'By Appt','Tue'=>'By Appt','Wed'=>'By Appt','Thu'=>'By Appt','Fri'=>'By Appt','Sat'=>'Special Events','Sun'=>'Special Events']),39.4378,-78.9734,0],
|
||||
// Churches
|
||||
['Keyser First United Methodist Church','keyser-umc',$CH,'Methodist Church',
|
||||
'Welcoming United Methodist congregation serving Keyser since the 19th century.',
|
||||
'32 N. Davis St., Keyser, WV 26726','304-788-2522','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'Closed','Thu'=>'Closed','Fri'=>'Closed','Sat'=>'Closed','Sun'=>'Worship 11am']),39.4372,-78.9730,0],
|
||||
['Living Faith Fellowship','living-faith',$CH,'Non-Denominational Church',
|
||||
'Vibrant non-denominational Christian fellowship. Contemporary worship and community ministry.',
|
||||
'1 N. Main St., Keyser, WV 26726','304-788-1910','','',
|
||||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'7pm','Thu'=>'Closed','Fri'=>'Closed','Sat'=>'Closed','Sun'=>'Worship 10:30am']),39.4376,-78.9734,0],
|
||||
];
|
||||
|
||||
foreach ($biz as $r) $bi->execute($r);
|
||||
|
||||
/* ── Menus ── */
|
||||
$si = $db->prepare("INSERT INTO menu_sections(business_id,name,description,sort_order)VALUES(?,?,?,?)");
|
||||
$mi = $db->prepare("INSERT INTO menu_items(section_id,name,description,price,sort_order)VALUES(?,?,?,?,?)");
|
||||
|
||||
$castId = (int)qval("SELECT id FROM businesses WHERE slug='castiglia-italian'");
|
||||
foreach ([['Antipasti & Salads','Fresh starters',1],['Pasta','Handmade pastas',2],['Pizza','Stone-baked Italian pizzas',3],['Entrees','Secondi piatti',4],['Desserts','Dolci',5]] as $s) $si->execute(array_merge([$castId],$s));
|
||||
$cs=[]; foreach(qall("SELECT id,name FROM menu_sections WHERE business_id=$castId") as $r) $cs[$r['name']]=$r['id'];
|
||||
foreach(['Antipasti & Salads'=>[['Bruschetta al Pomodoro','Toasted rustic bread with vine-ripened tomatoes, fresh basil & extra-virgin olive oil',8.95,1],['Calamari Fritti','Lightly breaded fresh calamari, marinara sauce & lemon aioli',13.95,2],['Caprese Salad','Fresh buffalo mozzarella, heirloom tomatoes, basil & balsamic glaze',11.95,3],['Minestrone Soup','Classic Italian vegetable soup with cannellini beans',7.50,4],['Caesar Salad','Romaine, house Caesar dressing, shaved Parmigiano, croutons',9.95,5]],
|
||||
'Pasta'=>[['Spaghetti Carbonara','Classic Roman — guanciale, egg, Pecorino Romano, cracked black pepper',15.95,1],['Fettuccine Alfredo','House-made fettuccine in rich cream & Parmigiano sauce',14.95,2],['Lasagna al Forno','House-rolled pasta, Bolognese, bechamel & Parmigiano',16.95,3],['Baked Ziti','Ziti, house marinara, ricotta & melted mozzarella',13.95,4],['Penne Arrabbiata','Spicy tomato, garlic, Calabrian chiles & fresh parsley',12.95,5]],
|
||||
'Pizza'=>[['Margherita','San Marzano tomatoes, fior di latte mozzarella & fresh basil',14.95,1],['Pepperoni','House marinara, mozzarella & premium pepperoni',15.95,2],['Quattro Stagioni','Ham, mushrooms, artichoke hearts & Kalamata olives',17.95,3],['Prosciutto & Arugula','Parma prosciutto, baby arugula & shaved Parmigiano',18.95,4]],
|
||||
'Entrees'=>[['Chicken Parmigiana','Breaded chicken, house marinara & fresh mozzarella, served with pasta',18.95,1],['Osso Buco alla Milanese','Braised veal shank, saffron risotto & traditional gremolata',34.95,2],['Eggplant Parmigiana','Layered eggplant, San Marzano sauce & mozzarella',15.95,3],['Salmon Piccata','Pan-seared Atlantic salmon, lemon-caper butter & fresh herbs',24.95,4]],
|
||||
'Desserts'=>[['Tiramisu','Espresso-soaked ladyfingers, mascarpone cream & cocoa',8.95,1],['Cannoli Siciliani','Crispy shells with sweet ricotta, chocolate chips & pistachios',7.95,2],['Panna Cotta','Silky vanilla panna cotta with seasonal berry coulis',7.50,3],['Gelato del Giorno','Two scoops house-made gelato — ask your server',6.95,4]]]
|
||||
as $sec=>$items) foreach($items as $item) $mi->execute(array_merge([$cs[$sec]],$item));
|
||||
|
||||
// Queens Point Coffee menu
|
||||
$qpId = (int)qval("SELECT id FROM businesses WHERE slug='queens-point-coffee'");
|
||||
foreach ([['Espresso Drinks','Hot & cold espresso beverages',1],['Cold Drinks','Refreshing chilled options',2],['Food','Fresh baked goods & light bites',3]] as $s) $si->execute(array_merge([$qpId],$s));
|
||||
$qs=[]; foreach(qall("SELECT id,name FROM menu_sections WHERE business_id=$qpId") as $r) $qs[$r['name']]=$r['id'];
|
||||
foreach(['Espresso Drinks'=>[['Latte','Smooth espresso with steamed milk',4.50,1],['Cappuccino','Double espresso with foamed milk',4.25,2],['Americano','Espresso shots with hot water',3.50,3],['Mocha','Espresso, chocolate & steamed milk',5.00,4],['Iced Matcha Latte','Matcha with milk — our most raved drink!',5.25,5]],
|
||||
'Cold Drinks'=>[['Cold Brew','Smooth 24-hour cold brew over ice',3.75,1],['Iced Latte','Chilled espresso with cold milk',4.75,2],['Blended Frappe','Blended ice coffee drink',5.50,3]],
|
||||
'Food'=>[['Blueberry Muffin','Fresh-baked daily',3.00,1],['Avocado Toast','Multigrain with avocado & everything spice',7.50,2],['Breakfast Sandwich','Egg, cheese & choice of meat',6.50,3],['Cookie','Assorted fresh-baked cookies',2.50,4]]]
|
||||
as $sec=>$items) foreach($items as $item) $mi->execute(array_merge([$qs[$sec]],$item));
|
||||
|
||||
/* ── Alerts ── */
|
||||
foreach ([
|
||||
['castiglia-italian','news','New Menu Items This Season!',
|
||||
"Chef's new handmade gnocchi and wood-fired seasonal pizza are now available. Fresh locally-sourced summer ingredients throughout the menu."],
|
||||
['queens-point-coffee','news','Try Our Famous Iced Matcha!',
|
||||
'"THE best coffee shop I\'ve ever been to." Our iced matcha with coconut and pistachio is the most-talked-about drink in Keyser.'],
|
||||
['clancys-irish-pub','event','Live Music Every Friday & Saturday!',
|
||||
"Live music from 9pm-1am every Friday and Saturday night. No cover charge! Food and drinks served until close."],
|
||||
['pvh','info','Urgent Care Now Open 7 Days',
|
||||
'WVU Medicine Potomac Valley Hospital Urgent Care is open Mon-Fri 8am-8pm and weekends 9am-5pm. No appointment needed.'],
|
||||
['north-branch-ventures','event','Guided Kayak Tours - Book Now!',
|
||||
"Guided half-day kayak tours on the North Branch Potomac every Saturday and Sunday through October. All skill levels welcome."],
|
||||
] as [$slug,$type,$title,$body])
|
||||
qrun("INSERT INTO alerts(business_id,type,title,body,is_active)VALUES((SELECT id FROM businesses WHERE slug=?),?,?,?,1)",[$slug,$type,$title,$body]);
|
||||
|
||||
/* ── Attractions ── */
|
||||
foreach ([
|
||||
['Queens Point Overlook','Natural Landmark',"A spectacular Oriskany sandstone cliff rising ~400 feet above the North Branch Potomac River. Breathtaking panoramic views of Keyser and the river valley.",'McCoole, MD (viewable from Keyser riverfront)','','','Accessible year-round',1,1],
|
||||
['North Branch Potomac River','Outdoor Recreation',"World-class smallmouth bass fishing, kayaking, canoeing, and bird watching. Contact North Branch Ventures for rentals and guided tours.",'Armstrong St. riverfront, Keyser, WV','304-790-7125','','Open year-round',1,2],
|
||||
['Jennings Randolph Lake','Lake / Reservoir',"A stunning 952-acre reservoir on the North Branch Potomac. Boating, fishing, swimming, camping, and picnicking. Managed by U.S. Army Corps of Engineers.",'Elk Garden, WV 26717 (~20 min from Keyser)','304-355-2346','','Open year-round',1,3],
|
||||
['Larenim Park','County Park',"Mineral County's premier park at 365 acres. Pavilions, 600-seat amphitheater, fishing ponds, and 5 miles of trails.",'Keyser, WV 26726','304-788-3066','','Open daily',1,4],
|
||||
['Mineral County Courthouse','Historic Site',"The 1868 courthouse listed on the National Register of Historic Places — the architectural centerpiece of downtown Keyser.",'Keyser, WV 26726','304-788-3003','','Exterior always viewable',1,5],
|
||||
['Potomac State College Campus / Fort Fuller','Historic Site',"Fort Hill — site of Civil War Fort Fuller — commanded by future president Benjamin Harrison and Lew Wallace (author of Ben-Hur). Now the beautiful PSC campus.",'101 Fort Ave., Keyser, WV 26726','304-788-6800','potomacstatecollege.edu','Campus open daily',1,6],
|
||||
['Potomac Eagle Scenic Railroad','Train Excursion',"Scenic excursions through The Trough along the South Branch Potomac River — famous for bald eagle sightings. About 30 miles from Keyser.",'Romney, WV 26757 (~30 miles)','304-822-7400','potomaceagle.info','Seasonal spring through fall',1,7],
|
||||
['Blackwater Falls State Park','State Park',"The amber-colored Blackwater Falls plunge 57 feet into the dramatic canyon. Hiking, skiing, camping, and a beautiful lodge. About 40 minutes from Keyser.",'Davis, WV 26260 (~40 min)','304-259-5216','wvstateparks.com','Open year-round',1,8],
|
||||
['Seneca Rocks','Natural Landmark',"Massive quartzite outcroppings rising 900 feet from the valley floor. Rock climbing, hiking, and spectacular views. About 45 minutes from Keyser.",'Seneca Rocks, WV (~45 min)','304-567-2827','','Open year-round',1,9],
|
||||
['Fort Ashby Blockhouse','Historic Site',"The only remaining French and Indian War fort in West Virginia. Ordered built by young George Washington in 1755. Just 15 miles from Keyser.",'Fort Ashby, WV 26719 (~15 miles)','','','Open seasonally',1,10],
|
||||
] as $a) qrun("INSERT INTO attractions(name,category,description,address,phone,website,hours,is_active,sort_order)VALUES(?,?,?,?,?,?,?,?,?)",$a);
|
||||
|
||||
/* ── Events ── */
|
||||
$adminId=(int)qval("SELECT id FROM users WHERE username='administrator'");
|
||||
foreach ([
|
||||
['Mineral County Fair','Annual county fair featuring livestock shows, carnival rides, food vendors, live entertainment, and the best of Mineral County agriculture.','Mineral County Fairgrounds','Fort Ashby, WV','2025-08-12','9:00 AM','2025-08-16','10:00 PM','Admission varies','mineralcountyfair.org','','','304-788-0000',$adminId,'approved',1],
|
||||
['Keyser Christmas Parade & Tree Lighting','Annual holiday parade through downtown Keyser with floats, marching bands, Santa Claus, and tree lighting.','Downtown Main Street','Keyser, WV 26726','2025-12-06','5:00 PM','2025-12-06','7:00 PM','Free','cityofkeyser.com','','','304-788-0222',$adminId,'approved',1],
|
||||
['PSC Catamount Athletics','Support the WVU Potomac State Catamounts at home athletic events throughout the semester.','Potomac State College','101 Fort Ave., Keyser','2025-09-01','Varies','2025-11-30','Varies','Free–$5','potomacstatecollege.edu','','','304-788-6800',$adminId,'approved',0],
|
||||
['North Branch Kayak Adventure Series','Guided and recreational kayaking on the North Branch Potomac every Saturday through October.','North Branch Ventures','101 Armstrong St., Keyser','2025-05-01','8:00 AM','2025-10-31','4:00 PM','$25–$45','','','','304-790-7125',$adminId,'approved',0],
|
||||
] as $ev) qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,submitted_by,status,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",$ev);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-grid">
|
||||
<div>
|
||||
<div class="f-logo">⛰ KEYSER, WV</div>
|
||||
<div class="f-tag">"<?=e(setting('site_tagline','The Friendliest City in the U.S.A.'))?>"</div>
|
||||
<p>County seat of Mineral County, WV. Where New Creek meets the North Branch Potomac River. Elevation 809 ft. Population ~4,800. Founded 1874. Part of the Appalachian Forest National Heritage Area.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5>Explore</h5>
|
||||
<ul>
|
||||
<li><a href="/directory.php">Business Directory</a></li>
|
||||
<li><a href="/attractions.php">Attractions</a></li>
|
||||
<li><a href="/events.php">Events Calendar</a></li>
|
||||
<li><a href="/about.php">About Keyser</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>Community</h5>
|
||||
<ul>
|
||||
<?php if (setting('registration_enabled','1')==='1'): ?>
|
||||
<li><a href="/register.php">Create Account</a></li>
|
||||
<?php endif; ?>
|
||||
<li><a href="/events.php?submit=1">Submit an Event</a></li>
|
||||
<li><a href="https://govisitmineralwv.com" target="_blank" rel="noopener">Mineral County Tourism ↗</a></li>
|
||||
<li><a href="https://cityofkeyser.com" target="_blank" rel="noopener">City of Keyser ↗</a></li>
|
||||
<li><a href="https://mineralwv.gov" target="_blank" rel="noopener">Mineral County Gov ↗</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>Tourism Office</h5>
|
||||
<address>
|
||||
<p>📍 167 S. Mineral Street<br>Keyser, WV 26726</p>
|
||||
<p>📞 <a href="tel:304-790-7081">304-790-7081</a></p>
|
||||
<p>✉️ <a href="mailto:mineralcocvb@gmail.com">mineralcocvb@gmail.com</a></p>
|
||||
<p>⏰ Mon–Fri 9am–4pm</p>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bar">
|
||||
<p>© <?=date('Y')?> My Keyser, Discover Keyser, West Virginia — Almost Heaven • Mountain State</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/assets/js/app.js"></script>
|
||||
<?=$footExtra??''?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/* ── Auth ─────────────────────────────────────────────── */
|
||||
function uid(): int { return (int)($_SESSION['uid'] ?? 0); }
|
||||
function uname(): string{ return (string)($_SESSION['uname'] ?? ''); }
|
||||
function authed(): bool { return uid() > 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 '<input type="hidden" name="_csrf" value="'.e(csrf()).'">'; }
|
||||
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 = '<span class="stars">';
|
||||
for ($i = 1; $i <= 5; $i++) $out .= '<span class="star'.($i<=$r?' on':'').'">★</span>';
|
||||
return $out.'</span>';
|
||||
}
|
||||
function starPicker(string $name = 'rating', int $sel = 0): string {
|
||||
$out = '<div class="star-picker">';
|
||||
for ($i = 5; $i >= 1; $i--) {
|
||||
$chk = $sel === $i ? 'checked' : '';
|
||||
$out .= '<input type="radio" id="sr'.$i.'" name="'.$name.'" value="'.$i.'" '.$chk.'>';
|
||||
$out .= '<label for="sr'.$i.'">★</label>';
|
||||
}
|
||||
return $out.'</div>';
|
||||
}
|
||||
|
||||
/* ── 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" : "");
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?=e($pageTitle??'Discover Keyser WV')?></title>
|
||||
<meta name="description" content="<?=e($pageDesc??'Discover Keyser, West Virginia — The Friendliest City in the U.S.A. Business directory, attractions, events and local information.')?>">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Oswald:wght@400;500;600&family=Source+Sans+3:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<?=$headExtra??''?>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar" id="navbar">
|
||||
<div class="nav-wrap">
|
||||
<a href="/index.php" class="nav-brand">
|
||||
<span class="brand-mtn">⛰</span>
|
||||
<div><span class="brand-city">KEYSER</span><span class="brand-wv">WEST VIRGINIA</span></div>
|
||||
</a>
|
||||
<button class="hamburger" id="ham" aria-label="Toggle navigation">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
<ul class="nav-links" id="navLinks">
|
||||
<li><a href="/index.php" class="nl<?=($activeNav??'')==='home'?' active':''?>">Home</a></li>
|
||||
<li><a href="/directory.php" class="nl<?=($activeNav??'')==='dir'?' active':''?>">Directory</a></li>
|
||||
<li><a href="/attractions.php" class="nl<?=($activeNav??'')==='attr'?' active':''?>">Attractions</a></li>
|
||||
<li><a href="/events.php" class="nl<?=($activeNav??'')==='events'?' active':''?>">Events</a></li>
|
||||
<li><a href="/about.php" class="nl<?=($activeNav??'')==='about'?' active':''?>">About</a></li>
|
||||
<?php if (authed()): ?>
|
||||
<?php if (isAdmin()): ?>
|
||||
<li><a href="/admin/" class="nl nl-admin">⚙ Admin</a></li>
|
||||
<?php else: ?>
|
||||
<li><a href="/dashboard.php" class="nl">My Listings</a></li>
|
||||
<?php endif; ?>
|
||||
<li class="user-wrap">
|
||||
<button class="user-btn" id="userBtn">👤 <?=e(uname())?> ▾</button>
|
||||
<div class="user-dd" id="userDd">
|
||||
<a href="/dashboard.php">Dashboard</a>
|
||||
<a href="/account.php">Account Settings</a>
|
||||
<hr>
|
||||
<a href="/logout.php">Sign Out</a>
|
||||
</div>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li><a href="/login.php" class="nl">Login</a></li>
|
||||
<?php if (setting('registration_enabled','1') === '1'): ?>
|
||||
<li><a href="/register.php" class="nl nl-join">Sign Up</a></li>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<?php foreach (getFlashes() as [$type,$msg]): ?>
|
||||
<div class="flash f-<?=e($type)?>" role="alert">
|
||||
<span><?=e($msg)?></span>
|
||||
<button onclick="this.parentElement.remove()" aria-label="Dismiss">✕</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<main id="top">
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
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");
|
||||
$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");
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<section class="hero">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-stars"></div>
|
||||
<div class="hero-mtns">
|
||||
<svg viewBox="0 0 1440 300" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="0,300 0,180 80,120 160,160 240,80 330,138 420,58 520,118 630,38 740,98 850,18 960,88 1060,28 1160,108 1260,48 1380,128 1440,78 1440,300" fill="#0a0f18" opacity=".9"/>
|
||||
<polygon points="0,300 0,222 100,170 200,200 310,140 430,188 560,128 680,168 800,100 920,158 1030,108 1140,158 1250,118 1380,175 1440,145 1440,300" fill="#0d1520" opacity=".96"/>
|
||||
<polygon points="0,300 0,260 140,232 280,252 420,212 560,242 700,202 840,235 980,197 1120,228 1260,205 1440,225 1440,300" fill="#111c2c"/>
|
||||
<g opacity=".28" stroke="#c9a84c" stroke-width="1.5" fill="none">
|
||||
<line x1="850" y1="20" x2="850" y2="72"/><line x1="850" y1="20" x2="833" y2="8"/><line x1="850" y1="20" x2="867" y2="8"/><line x1="850" y1="20" x2="850" y2="5"/>
|
||||
<line x1="903" y1="30" x2="903" y2="82"/><line x1="903" y1="30" x2="886" y2="18"/><line x1="903" y1="30" x2="920" y2="18"/><line x1="903" y1="30" x2="903" y2="15"/>
|
||||
<line x1="958" y1="42" x2="958" y2="94"/><line x1="958" y1="42" x2="941" y2="30"/><line x1="958" y1="42" x2="975" y2="30"/><line x1="958" y1="42" x2="958" y2="27"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-eyebrow">⛰ MINERAL COUNTY · EASTERN PANHANDLE · WEST VIRGINIA</div>
|
||||
<h1 class="hero-h1">DISCOVER<span class="gld">KEYSER</span></h1>
|
||||
<div class="hero-sub">NORTH BRANCH POTOMAC RIVER · FOUNDED 1874</div>
|
||||
<div class="hero-rule"></div>
|
||||
<p class="hero-desc">Where Appalachian heritage meets mountain beauty. County seat of Mineral County — 3 hours from Washington D.C., a world away from ordinary.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/directory.php" class="btn btn-primary">Explore Businesses</a>
|
||||
<a href="/attractions.php" class="btn btn-outline">Attractions & Activities</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="stats-strip">
|
||||
<div class="stats-inner">
|
||||
<?php foreach([['1874','Founded'],['809 ft','Elevation'],['~4,800','Population'],['132','Wind Turbines'],['1901','PSC Founded'],[$totalBiz,'Businesses Listed']] as [$n,$l]): ?>
|
||||
<div class="stat"><div class="stat-n"><?=e($n)?></div><div class="stat-l"><?=e($l)?></div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($alerts): ?>
|
||||
<div class="alert-strip">
|
||||
<div class="alert-strip-inner">
|
||||
<div class="eyebrow" style="margin-bottom:.65rem">📢 LOCAL BUSINESS NEWS & UPDATES</div>
|
||||
<?php foreach($alerts as $a): ?>
|
||||
<div class="alert-item a-<?=e($a['type'])?>">
|
||||
<div class="alert-ico"><?=alertIcon($a['type'] ?? '')?></div>
|
||||
<div>
|
||||
<div class="alert-title"><?=e($a['title'])?></div>
|
||||
<div class="alert-body"><?=e($a['body'])?></div>
|
||||
<div class="alert-meta">From <a href="/business.php?slug=<?=e($a['bslug'])?>"><?=e($a['bname'])?></a> · <?=ago($a['created_at'] ?? '')?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="section">
|
||||
<div style="margin-bottom:2rem">
|
||||
<div class="eyebrow">⭐A LITTLE SPOTLIGHT</div>
|
||||
<h2 class="sec-title">Featured</h2>
|
||||
<div class="sec-rule"></div>
|
||||
</div>
|
||||
<div class="g3">
|
||||
<?php foreach($featured as $b): ?>
|
||||
<a href="/business.php?slug=<?=e($b['slug'])?>" style="text-decoration:none">
|
||||
<div class="biz-card">
|
||||
<div class="biz-card-top">
|
||||
<div class="cat-badge"><?=e($b['cat_icon'].' '.$b['cat'])?></div>
|
||||
<?php if($b['is_featured']): ?><span class="featured-badge" style="float:right">★ FEATURED</span><?php endif; ?>
|
||||
<div class="biz-name"><?=e($b['name'])?></div>
|
||||
<?php if($b['subcategory']): ?><div class="biz-sub"><?=e($b['subcategory'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="biz-card-body">
|
||||
<div class="biz-desc"><?=e($b['description'])?></div>
|
||||
<div class="biz-meta">
|
||||
<?php if($b['address']): ?><div class="biz-meta-row"><span class="biz-meta-icon">📍</span><?=e($b['address'])?></div><?php endif; ?>
|
||||
<?php if($b['phone']): ?><div class="biz-meta-row"><span class="biz-meta-icon">📞</span><?=e($b['phone'])?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="biz-card-foot">
|
||||
<div class="flex-row" style="gap:.4rem">
|
||||
<?=starDisplay((float)$b['avg'])?>
|
||||
<span class="rat-score"><?=number_format((float)$b['avg'],1)?></span>
|
||||
<span class="rat-cnt">(<?=$b['rcnt']?>)</span>
|
||||
</div>
|
||||
<span style="color:var(--gold);font-size:.76rem;font-family:'Oswald',sans-serif">VIEW →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:2rem">
|
||||
<a href="/directory.php" class="btn btn-outline">Browse Full Directory (<?=$totalBiz?> Businesses)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background:var(--bg2);border-top:1px solid var(--bdr);border-bottom:1px solid var(--bdr);padding:4rem 1.5rem">
|
||||
<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr 1fr;gap:3rem;align-items:center">
|
||||
<div>
|
||||
<div class="eyebrow">OUR HISTORY</div>
|
||||
<h2 class="sec-title" style="margin-bottom:1rem">A City with Deep Mountain Roots</h2>
|
||||
<div class="prose">
|
||||
<p>Originally called <strong>Paddy Town</strong> 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.</p>
|
||||
<p>Today Keyser is home to <strong>Potomac State College of WVU</strong> (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.</p>
|
||||
<p>Mineral County is part of the <strong>Appalachian Forest National Heritage Area</strong> — over 500 square miles of forested beauty where American history lives on in every ridge and hollow.</p>
|
||||
</div>
|
||||
<a href="/about.php" class="btn btn-outline" style="margin-top:1rem">Learn More About Keyser</a>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<?php foreach([['🏛️','HISTORIC','National Register of Historic Places sites'],['🎓','EDUCATION','Potomac State College of WVU since 1901'],['🌊','OUTDOORS','Kayaking & fishing the North Branch'],['💨','WIND ENERGY','132 turbines on the Allegheny Front']] as [$ic,$lbl,$val]): ?>
|
||||
<div style="background:var(--bg3);border:1px solid var(--bdr);border-radius:6px;padding:1.2rem;text-align:center">
|
||||
<div style="font-size:2rem;margin-bottom:.45rem"><?=$ic?></div>
|
||||
<div style="font-family:'Oswald',sans-serif;font-size:.66rem;letter-spacing:.2em;color:var(--gold)"><?=$lbl?></div>
|
||||
<div style="color:var(--text);margin-top:.25rem;font-size:.88rem"><?=$val?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if($upevts): ?>
|
||||
<div class="section">
|
||||
<div style="margin-bottom:2rem">
|
||||
<div class="eyebrow">📅 WHAT'S HAPPENING</div>
|
||||
<h2 class="sec-title">Upcoming Events</h2>
|
||||
<div class="sec-rule"></div>
|
||||
</div>
|
||||
<?php foreach($upevts as $ev): ?>
|
||||
<div class="evt-card">
|
||||
<div class="evt-date-box">
|
||||
<div class="evt-mon"><?=strtoupper(date('M',strtotime($ev['event_date'])))?></div>
|
||||
<div class="evt-day"><?=date('j',strtotime($ev['event_date']))?></div>
|
||||
<div class="evt-yr"><?=date('Y',strtotime($ev['event_date']))?></div>
|
||||
</div>
|
||||
<div class="evt-body">
|
||||
<div class="evt-title"><?=e($ev['title'])?><?php if($ev['is_featured']): ?> <span class="featured-badge" style="margin-left:.5rem">★ FEATURED</span><?php endif; ?></div>
|
||||
<div class="evt-meta">
|
||||
<?php if($ev['venue']): ?><span>📍 <?=e($ev['venue'])?></span><?php endif; ?>
|
||||
<?php if($ev['event_time']): ?><span>🕐 <?=e($ev['event_time'])?></span><?php endif; ?>
|
||||
<span>💰 <?=e($ev['cost'])?></span>
|
||||
</div>
|
||||
<div class="evt-desc"><?=e(substr($ev['description'],0,180)).(strlen($ev['description'])>180?'…':'')?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div style="text-align:center;margin-top:1.5rem">
|
||||
<a href="/events.php" class="btn btn-outline">All Events</a>
|
||||
<a href="/events.php?submit=1" class="btn btn-secondary" style="margin-left:.75rem">Submit an Event</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="padding:4rem 1.5rem;text-align:center;max-width:560px;margin:0 auto">
|
||||
<div class="eyebrow">JOIN THE COMMUNITY</div>
|
||||
<h2 style="font-size:2rem;margin:.5rem 0 1rem">Own a Business in Keyser?</h2>
|
||||
<p style="color:var(--text-m);margin-bottom:2rem;line-height:1.85">Create an account to manage your listing, post alerts, add menus, and connect with the Keyser community.</p>
|
||||
<div class="flex-row" style="justify-content:center">
|
||||
<?php if(!authed()): ?>
|
||||
<a href="/register.php" class="btn btn-primary">Create Account</a>
|
||||
<a href="/login.php" class="btn btn-outline">Login</a>
|
||||
<?php else: ?>
|
||||
<a href="/dashboard.php" class="btn btn-primary">My Dashboard</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
if (authed()) go('/index.php');
|
||||
$pageTitle = 'Login — Keyser WV';
|
||||
$next = gs('next') ?: '/index.php';
|
||||
$err = '';
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$un = ps('username'); $pw = ps('password');
|
||||
$u = qone("SELECT * FROM users WHERE (username=? OR email=?) AND is_active=1", [$un, $un]);
|
||||
if ($u && password_verify($pw, $u['password'])) {
|
||||
loginUser($u);
|
||||
flash('Welcome back, '.e($u['username']).'!', 'success');
|
||||
go($next);
|
||||
} else {
|
||||
$err = 'Invalid username or password.';
|
||||
}
|
||||
}
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="form-box">
|
||||
<h1 class="form-title">Sign In</h1>
|
||||
<p class="form-sub">Welcome back to Discover Keyser WV</p>
|
||||
<?php if ($err): ?><div class="err-box"><?=e($err)?></div><?php endif; ?>
|
||||
<form method="POST">
|
||||
<?=csrfField()?>
|
||||
<div class="fg"><label class="flabel">USERNAME OR EMAIL</label><input type="text" name="username" class="finput" autocomplete="username" required autofocus></div>
|
||||
<div class="fg"><label class="flabel">PASSWORD</label><input type="password" name="password" class="finput" autocomplete="current-password" required></div>
|
||||
<button type="submit" class="btn btn-primary btn-block" style="margin-top:.5rem">Sign In</button>
|
||||
</form>
|
||||
<?php if (setting('registration_enabled','1') === '1'): ?>
|
||||
<p style="text-align:center;margin-top:1.25rem;font-size:.88rem;color:var(--text-m)">Don't have an account? <a href="/register.php">Sign up free</a></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
logoutUser();
|
||||
flash('You have been signed out.','info');
|
||||
go('/index.php');
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
requireLogin();
|
||||
$bizId=iget('id');
|
||||
$biz=qone("SELECT * FROM businesses WHERE id=?",[$bizId]);
|
||||
if(!$biz||!owns($bizId)){flash('Access denied.','error');go('/dashboard.php');}
|
||||
|
||||
if(isPost()){
|
||||
csrfCheck();
|
||||
$act=p('_act');
|
||||
if($act==='add_section'){
|
||||
$name=ps('sec_name');
|
||||
if($name){
|
||||
$order=(int)qval("SELECT COALESCE(MAX(sort_order),0)+1 FROM menu_sections WHERE business_id=?",[$bizId]);
|
||||
qrun("INSERT INTO menu_sections(business_id,name,description,sort_order)VALUES(?,?,?,?)",[$bizId,$name,ps('sec_desc'),$order]);
|
||||
flash('Section added!','success');
|
||||
}
|
||||
}
|
||||
if($act==='del_section'){
|
||||
qrun("DELETE FROM menu_sections WHERE id=? AND business_id=?",[(int)p('sid'),$bizId]);
|
||||
flash('Section deleted.','success');
|
||||
}
|
||||
if($act==='edit_section'){
|
||||
qrun("UPDATE menu_sections SET name=?,description=? WHERE id=? AND business_id=?",[ps('sec_name'),ps('sec_desc'),(int)p('sid'),$bizId]);
|
||||
flash('Section updated!','success');
|
||||
}
|
||||
if($act==='add_item'){
|
||||
$sid=(int)p('section_id');
|
||||
$name=ps('item_name');
|
||||
if($name){
|
||||
$order=(int)qval("SELECT COALESCE(MAX(sort_order),0)+1 FROM menu_items WHERE section_id=?",[$sid]);
|
||||
qrun("INSERT INTO menu_items(section_id,name,description,price,sort_order)VALUES(?,?,?,?,?)",[$sid,$name,ps('item_desc'),(float)p('item_price',0),$order]);
|
||||
flash('Item added!','success');
|
||||
}
|
||||
}
|
||||
if($act==='del_item'){
|
||||
qrun("DELETE FROM menu_items WHERE id=?",[(int)p('iid')]);
|
||||
flash('Item removed.','success');
|
||||
}
|
||||
if($act==='edit_item'){
|
||||
$iid=(int)p('iid');
|
||||
qrun("UPDATE menu_items SET name=?,description=?,price=?,is_available=? WHERE id=?",[ps('item_name'),ps('item_desc'),(float)p('item_price',0),(int)p('is_available',1),$iid]);
|
||||
flash('Item updated!','success');
|
||||
}
|
||||
go("/menu.php?id=$bizId");
|
||||
}
|
||||
|
||||
$pageTitle='Manage Menu — '.e($biz['name']);
|
||||
$sections=qall("SELECT * FROM menu_sections WHERE business_id=? ORDER BY sort_order",[$bizId]);
|
||||
$items=[]; foreach($sections as $s) $items[$s['id']]=qall("SELECT * FROM menu_items WHERE section_id=? ORDER BY sort_order",[$s['id']]);
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner">
|
||||
<div class="eyebrow">OWNER PANEL</div>
|
||||
<h1 style="font-size:1.8rem;margin-bottom:.3rem">Manage Menu</h1>
|
||||
<p style="color:var(--text-m)"><?=e($biz['name'])?> · <a href="/business.php?slug=<?=e($biz['slug'])?>">View Listing</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" style="max-width:960px">
|
||||
<!-- Add Section -->
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">➕ ADD MENU SECTION</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add_section">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">SECTION NAME *</label><input type="text" name="sec_name" class="finput" placeholder="e.g. Appetizers, Pasta, Desserts" required></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION (optional)</label><input type="text" name="sec_desc" class="finput" placeholder="Brief description…"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add Section</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php foreach($sections as $sec): ?>
|
||||
<div class="ibox" style="margin-bottom:1.5rem">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.9rem;padding-bottom:.6rem;border-bottom:1px solid var(--bdr)">
|
||||
<div>
|
||||
<div style="font-family:'Oswald',sans-serif;letter-spacing:.1em;font-size:1rem;color:var(--gold)"><?=e($sec['name'])?></div>
|
||||
<?php if($sec['description']): ?><div style="font-size:.78rem;color:var(--text-d)"><?=e($sec['description'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="flex-row" style="gap:.4rem">
|
||||
<button onclick="this.closest('.ibox').querySelector('.edit-sec-form').classList.toggle('hidden')" class="btn btn-xs btn-secondary">✏️ Edit</button>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="del_section"><input type="hidden" name="sid" value="<?=$sec['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete section and ALL its items?">🗑️ Delete</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Edit section form (hidden by default) -->
|
||||
<div class="edit-sec-form hidden" style="background:var(--bg4);border:1px solid var(--bdr);border-radius:4px;padding:1rem;margin-bottom:1rem">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="edit_section"><input type="hidden" name="sid" value="<?=$sec['id']?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">NAME</label><input type="text" name="sec_name" class="finput" value="<?=e($sec['name'])?>" required></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="sec_desc" class="finput" value="<?=e($sec['description'])?>"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success btn-sm">Save Section</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
<?php foreach($items[$sec['id']] as $item): ?>
|
||||
<div style="border-bottom:1px solid var(--bdr);padding:.75rem 0">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem">
|
||||
<div>
|
||||
<div style="font-weight:600;color:<?=$item['is_available']?'var(--text)':'var(--text-d)'?>"><?=e($item['name'])?> <?php if(!$item['is_available']): ?><span class="badge badge-gray">Unavailable</span><?php endif; ?></div>
|
||||
<?php if($item['description']): ?><div style="font-size:.8rem;color:var(--text-d)"><?=e($item['description'])?></div><?php endif; ?>
|
||||
<?php if($item['price']>0): ?><div style="color:var(--gold);font-family:'Oswald',sans-serif;font-size:.9rem"><?=money((float)$item['price'])?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="flex-row" style="gap:.3rem;flex-shrink:0">
|
||||
<button onclick="this.closest('div').parentElement.querySelector('.edit-item-form').classList.toggle('hidden')" class="btn btn-xs btn-secondary">✏️</button>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="del_item"><input type="hidden" name="iid" value="<?=$item['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete this item?">🗑️</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-item-form hidden" style="background:var(--bg4);border:1px solid var(--bdr);border-radius:4px;padding:.85rem;margin-top:.65rem">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="edit_item"><input type="hidden" name="iid" value="<?=$item['id']?>">
|
||||
<div class="form-row" style="margin-bottom:.6rem">
|
||||
<div class="fg"><label class="flabel">NAME</label><input type="text" name="item_name" class="finput" value="<?=e($item['name'])?>" required></div>
|
||||
<div class="fg"><label class="flabel">PRICE ($)</label><input type="number" name="item_price" class="finput" value="<?=e($item['price'])?>" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" value="<?=e($item['description'])?>"></div>
|
||||
<div class="fg"><label class="flabel">AVAILABLE</label><select name="is_available" class="fselect"><option value="1"<?=$item['is_available']?' selected':''?>>Yes</option><option value="0"<?=!$item['is_available']?' selected':''?>>No (hidden from menu)</option></select></div>
|
||||
<button type="submit" class="btn btn-success btn-sm">Save Item</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- Add item to this section -->
|
||||
<div style="margin-top:.85rem;padding-top:.85rem">
|
||||
<details><summary style="cursor:pointer;font-size:.82rem;color:var(--gold);font-family:'Oswald',sans-serif;letter-spacing:.08em">➕ Add Item to "<?=e($sec['name'])?>"</summary>
|
||||
<div style="padding:.85rem 0">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add_item"><input type="hidden" name="section_id" value="<?=$sec['id']?>">
|
||||
<div class="form-row" style="margin-bottom:.6rem">
|
||||
<div class="fg"><label class="flabel">ITEM NAME *</label><input type="text" name="item_name" class="finput" required placeholder="e.g. Spaghetti Carbonara"></div>
|
||||
<div class="fg"><label class="flabel">PRICE ($)</label><input type="number" name="item_price" class="finput" value="0" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" placeholder="Brief description…"></div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add Item</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if(!$sections): ?>
|
||||
<div class="empty"><div class="empty-ico">🍽️</div><h3>No menu sections yet</h3><p style="margin-top:.5rem">Add a section above to start building your menu.</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="/business.php?slug=<?=e($biz['slug'])?>" class="btn btn-outline">← Back to Listing</a>
|
||||
</div>
|
||||
<style>.hidden{display:none!important}</style>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
if (authed()) go('/index.php');
|
||||
if (setting('registration_enabled','1') !== '1') {
|
||||
flash('Registration is currently disabled. Please check back later.','warning');
|
||||
go('/login.php');
|
||||
}
|
||||
|
||||
$pageTitle = 'Create Account — Discover Keyser WV';
|
||||
$errors = [];
|
||||
|
||||
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)
|
||||
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');
|
||||
}
|
||||
|
||||
$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.';
|
||||
|
||||
if (!$errors) {
|
||||
$id = qrun(
|
||||
"INSERT INTO users(username,email,password)VALUES(?,?,?)",
|
||||
[$un, $em ?: null, password_hash($pw, PASSWORD_BCRYPT)]
|
||||
);
|
||||
loginUser(qone("SELECT * FROM users WHERE id=?", [$id]));
|
||||
flash('Welcome to Discover Keyser WV, '.e($un).'!', 'success');
|
||||
go('/index.php');
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
/* Honeypot field — visually gone, still in DOM for bots */
|
||||
.hp-field {
|
||||
position:absolute;
|
||||
left:-9999px;
|
||||
top:-9999px;
|
||||
opacity:0;
|
||||
height:0;
|
||||
width:0;
|
||||
overflow:hidden;
|
||||
pointer-events:none;
|
||||
tabindex:-1;
|
||||
}
|
||||
.benefit-grid {
|
||||
display:grid;
|
||||
grid-template-columns:1fr 1fr;
|
||||
gap:1rem;
|
||||
margin-bottom:2rem;
|
||||
}
|
||||
@media(max-width:640px){.benefit-grid{grid-template-columns:1fr}}
|
||||
.benefit-card {
|
||||
background:var(--bg3);
|
||||
border:1px solid var(--bdr);
|
||||
border-radius:var(--r);
|
||||
padding:1.4rem;
|
||||
}
|
||||
.benefit-card h3 {
|
||||
font-family:'Oswald',sans-serif;
|
||||
font-size:.88rem;
|
||||
letter-spacing:.14em;
|
||||
color:var(--gold);
|
||||
margin-bottom:.85rem;
|
||||
padding-bottom:.55rem;
|
||||
border-bottom:1px solid var(--bdr);
|
||||
}
|
||||
.benefit-card ul {
|
||||
list-style:none;
|
||||
padding:0;
|
||||
margin:0;
|
||||
}
|
||||
.benefit-card ul li {
|
||||
display:flex;
|
||||
gap:.55rem;
|
||||
align-items:flex-start;
|
||||
font-size:.86rem;
|
||||
color:var(--text-m);
|
||||
line-height:1.55;
|
||||
margin-bottom:.55rem;
|
||||
}
|
||||
.benefit-card ul li:last-child{margin-bottom:0}
|
||||
.benefit-card ul li .bi {flex-shrink:0;margin-top:1px}
|
||||
.owner-note {
|
||||
background:rgba(26,58,92,.25);
|
||||
border:1px solid rgba(42,95,153,.4);
|
||||
border-radius:var(--r);
|
||||
padding:1.1rem 1.3rem;
|
||||
font-size:.86rem;
|
||||
color:var(--text-m);
|
||||
line-height:1.7;
|
||||
margin-bottom:2rem;
|
||||
}
|
||||
.owner-note strong {color:var(--gold-l)}
|
||||
.tos-check {
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
gap:.65rem;
|
||||
background:var(--bg4);
|
||||
border:1px solid var(--bdr-l);
|
||||
border-radius:var(--r);
|
||||
padding:1rem 1.15rem;
|
||||
cursor:pointer;
|
||||
transition:border-color var(--t);
|
||||
}
|
||||
.tos-check:has(input:checked) {border-color:var(--green-l)}
|
||||
.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)}
|
||||
</style>
|
||||
|
||||
<div style="max-width:860px;margin:3rem auto;padding:0 1.5rem">
|
||||
|
||||
<!-- Page header -->
|
||||
<div style="text-align:center;margin-bottom:2rem">
|
||||
<div class="eyebrow" style="justify-content:center;display:flex">JOIN THE COMMUNITY</div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.8rem);margin:.4rem 0">Create Your Account</h1>
|
||||
<p style="color:var(--text-m);font-size:1rem;max-width:520px;margin:.6rem auto 0;line-height:1.7">
|
||||
Discover Keyser WV is your community hub for local businesses, events, and information.
|
||||
Takes less than a minute.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Benefits -->
|
||||
<div class="benefit-grid">
|
||||
<div class="benefit-card">
|
||||
<h3>👤 COMMUNITY MEMBER BENEFITS</h3>
|
||||
<ul>
|
||||
<li><span class="bi">⭐</span>Rate and review local Keyser businesses</li>
|
||||
<li><span class="bi">📅</span>Submit community events to the public calendar</li>
|
||||
<li><span class="bi">🚩</span>Report incorrect or outdated business listings</li>
|
||||
<li><span class="bi">💬</span>Share your experiences with the Keyser community</li>
|
||||
<li><span class="bi">📍</span>Access your personal dashboard with your submissions</li>
|
||||
<li><span class="bi">🔔</span>Stay connected with what's happening in Mineral County</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="benefit-card" style="border-color:rgba(26,72,118,.5)">
|
||||
<h3>🏢 BUSINESS OWNER BENEFITS</h3>
|
||||
<ul>
|
||||
<li><span class="bi">✏️</span>Update your listing — description, hours, phone, address</li>
|
||||
<li><span class="bi">📢</span>Post alerts and announcements directly on your business page</li>
|
||||
<li><span class="bi">🍽️</span>Build and manage a full menu for your restaurant or café</li>
|
||||
<li><span class="bi">🟢</span>Mark your business open or temporarily closed in real time</li>
|
||||
<li><span class="bi">📊</span>View your listing's reviews and ratings from one dashboard</li>
|
||||
<li><span class="bi">🔑</span>All changes are reviewed by our admin team before going live</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business owner request note -->
|
||||
<div class="owner-note">
|
||||
<strong>🏢 Do you own or manage a Keyser business?</strong> Create your account below,
|
||||
then <strong>contact our admin team</strong> at
|
||||
<a href="mailto:<?=e(setting('contact_email','info@discoverkeyser.com'))?>" style="color:var(--gold-l)"><?=e(setting('contact_email','info@discoverkeyser.com'))?></a>
|
||||
with your username and the name of your business.
|
||||
An administrator will link your account to your listing so you can start managing it.
|
||||
This verification step helps us ensure only legitimate owners control business pages.
|
||||
</div>
|
||||
|
||||
<!-- Registration form -->
|
||||
<div style="background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r-lg);padding:2.25rem">
|
||||
<h2 style="font-family:'Oswald',sans-serif;font-size:1rem;letter-spacing:.14em;color:var(--text);margin-bottom:1.5rem;padding-bottom:.65rem;border-bottom:1px solid var(--bdr)">
|
||||
CREATE YOUR ACCOUNT
|
||||
</h2>
|
||||
|
||||
<?php if ($errors): ?>
|
||||
<div class="err-box" style="margin-bottom:1.25rem"><?=implode('<br>', array_map('e', $errors))?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" autocomplete="off">
|
||||
<?=csrfField()?>
|
||||
<!-- Timing token -->
|
||||
<input type="hidden" name="_form_time" value="<?=time()?>">
|
||||
|
||||
<!-- ── HONEYPOT fields — hidden from humans, filled by bots ── -->
|
||||
<div class="hp-field" aria-hidden="true">
|
||||
<label for="website_url">Leave this blank</label>
|
||||
<input type="text" id="website_url" name="website_url" value=""
|
||||
tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
<div class="hp-field" aria-hidden="true">
|
||||
<label for="confirm_email">Do not fill</label>
|
||||
<input type="email" id="confirm_email" name="confirm_email" value=""
|
||||
tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
<!-- ── END HONEYPOT ─────────────────────────────────────────── -->
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<div class="fg" style="margin-bottom:0">
|
||||
<label class="flabel" for="username">USERNAME *</label>
|
||||
<input type="text" id="username" name="username" class="finput"
|
||||
value="<?=e($_POST['username']??'')?>"
|
||||
autocomplete="username" required autofocus
|
||||
pattern="[a-zA-Z0-9_]+" minlength="3">
|
||||
<div class="fhint">Letters, numbers, underscores only. Min 3 characters.</div>
|
||||
</div>
|
||||
<div class="fg" style="margin-bottom:0">
|
||||
<label class="flabel" for="email">EMAIL ADDRESS <span style="font-weight:300;letter-spacing:0">(optional)</span></label>
|
||||
<input type="email" id="email" name="email" class="finput"
|
||||
value="<?=e($_POST['email']??'')?>"
|
||||
autocomplete="email">
|
||||
<div class="fhint">Used only for account recovery. Never shared.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:1rem">
|
||||
<div class="fg" style="margin-bottom:0">
|
||||
<label class="flabel" for="password">PASSWORD *</label>
|
||||
<input type="password" id="password" name="password" class="finput"
|
||||
autocomplete="new-password" required minlength="6">
|
||||
<div class="fhint">Minimum 6 characters.</div>
|
||||
</div>
|
||||
<div class="fg" style="margin-bottom:0">
|
||||
<label class="flabel" for="password2">CONFIRM PASSWORD *</label>
|
||||
<input type="password" id="password2" name="password2" class="finput"
|
||||
autocomplete="new-password" required minlength="6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms / lawful use checkbox -->
|
||||
<div style="margin:1.5rem 0 1.25rem">
|
||||
<label class="tos-check">
|
||||
<input type="checkbox" name="lawful_use" value="1"
|
||||
<?=(!empty($_POST) && p('lawful_use')==='1') ? 'checked' : ''?> required>
|
||||
<span class="tos-check-text">
|
||||
<strong>I will only use this service lawfully.</strong>
|
||||
I agree not to post false, misleading, defamatory, or harmful content.
|
||||
I understand that accounts used for spam, abuse, or fraudulent business claims
|
||||
will be removed without notice.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block" style="font-size:.9rem;padding:.85rem">
|
||||
Create Account
|
||||
</button>
|
||||
|
||||
<p style="text-align:center;margin-top:1.1rem;font-size:.84rem;color:var(--text-d)">
|
||||
Already have an account? <a href="/login.php">Sign in here</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
Reference in New Issue
Block a user