- initial

This commit is contained in:
Ty Clifford
2026-04-24 08:41:52 -04:00
commit b57091e4ad
49 changed files with 7371 additions and 0 deletions
+842
View File
@@ -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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5pm','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. WedThu 11am9pm, FriSat 11am9pm, Sun noon9pm.",
'401 S. Mineral St., Keyser, WV 26726','304-788-1300','','',
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am9pm','Sat'=>'11am9pm','Sun'=>'12pm9pm']),
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'=>'5pm9pm','Wed'=>'5pm9pm','Thu'=>'5pm9pm','Fri'=>'5pm10pm','Sat'=>'4pm10pm','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'=>'4pm12am','Tue'=>'4pm12am','Wed'=>'4pm12am','Thu'=>'4pm12am','Fri'=>'4pm2am','Sat'=>'12pm2am','Sun'=>'12pm10pm']),
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'=>'4pm11pm','Wed'=>'4pm11pm','Thu'=>'4pm11pm','Fri'=>'3pm1am','Sat'=>'12pm1am','Sun'=>'12pm8pm']),
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'=>'7am3pm','Tue'=>'7am5pm','Wed'=>'7am5pm','Thu'=>'7am5pm','Fri'=>'7am5pm','Sat'=>'8am5pm','Sun'=>'9am2pm']),
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'=>'6am8pm','Tue'=>'6am8pm','Wed'=>'6am8pm','Thu'=>'6am8pm','Fri'=>'6am9pm','Sat'=>'6am9pm','Sun'=>'7am3pm']),
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'=>'11am9pm','Tue'=>'11am9pm','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am10pm','Sat'=>'11am10pm','Sun'=>'12pm9pm']),
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'=>'6am11pm','Tue'=>'6am11pm','Wed'=>'6am11pm','Thu'=>'6am11pm','Fri'=>'6am12am','Sat'=>'6am12am','Sun'=>'7am11pm']),
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'=>'6am12am','Tue'=>'6am12am','Wed'=>'6am12am','Thu'=>'6am12am','Fri'=>'6am1am','Sat'=>'6am1am','Sun'=>'7am12am']),
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:30am12am','Tue'=>'10:30am12am','Wed'=>'10:30am12am','Thu'=>'10:30am12am','Fri'=>'10:30am1am','Sat'=>'10:30am1am','Sun'=>'10:30am12am']),
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'=>'10am10pm','Tue'=>'10am10pm','Wed'=>'10am10pm','Thu'=>'10am10pm','Fri'=>'10am10:30pm','Sat'=>'10am10:30pm','Sun'=>'11am9pm']),
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'=>'11am10pm','Tue'=>'11am10pm','Wed'=>'11am10pm','Thu'=>'11am10pm','Fri'=>'11am11pm','Sat'=>'11am11pm','Sun'=>'12pm9pm']),
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'=>'11am9pm','Tue'=>'11am9pm','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am10pm','Sat'=>'11am10pm','Sun'=>'12pm9pm']),
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'=>'7am10pm','Tue'=>'7am10pm','Wed'=>'7am10pm','Thu'=>'7am10pm','Fri'=>'7am10pm','Sat'=>'8am10pm','Sun'=>'9am9pm']),
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'=>'7am12am','Tue'=>'7am12am','Wed'=>'7am12am','Thu'=>'7am12am','Fri'=>'7am2am','Sat'=>'7am2am','Sun'=>'8am12am']),
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'=>'11am11pm','Tue'=>'11am11pm','Wed'=>'11am11pm','Thu'=>'11am11pm','Fri'=>'11am1am','Sat'=>'11am1am','Sun'=>'12pm9pm']),
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'=>'11am10pm','Tue'=>'11am10pm','Wed'=>'11am10pm','Thu'=>'11am10pm','Fri'=>'11am12am','Sat'=>'11am12am','Sun'=>'12pm9pm']),
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'=>'11am9pm','Tue'=>'11am9pm','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am10pm','Sat'=>'11am10pm','Sun'=>'12pm9pm']),
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'=>'11am9pm','Tue'=>'11am9pm','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am10pm','Sat'=>'11am10pm','Sun'=>'12pm9pm']),
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'=>'10am6pm','Tue'=>'10am6pm','Wed'=>'10am6pm','Thu'=>'10am6pm','Fri'=>'10am7pm','Sat'=>'10am7pm','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'=>'7am5pm','Tue'=>'7am5pm','Wed'=>'7am5pm','Thu'=>'7am5pm','Fri'=>'7am5pm','Sat'=>'8am4pm','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'=>'6am4pm','Tue'=>'6am4pm','Wed'=>'6am4pm','Thu'=>'6am4pm','Fri'=>'6am4pm','Sat'=>'7am3pm','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'=>'4pm10pm','Thu'=>'4pm10pm','Fri'=>'3pm12am','Sat'=>'12pm12am','Sun'=>'12pm8pm']),
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'=>'4pm7pm','Sat'=>'9am1pm','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'=>'6am10pm','Tue'=>'6am10pm','Wed'=>'6am10pm','Thu'=>'6am10pm','Fri'=>'6am10pm','Sat'=>'6am10pm','Sun'=>'7am9pm']),
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'=>'8am6pm','Tue'=>'8am6pm','Wed'=>'8am6pm','Thu'=>'8am6pm','Fri'=>'8am6pm','Sat'=>'8am5pm','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'=>'8am9pm','Tue'=>'8am9pm','Wed'=>'8am9pm','Thu'=>'8am9pm','Fri'=>'8am9pm','Sat'=>'8am9pm','Sun'=>'9am7pm']),
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:30am9pm','Tue'=>'7:30am9pm','Wed'=>'7:30am9pm','Thu'=>'7:30am9pm','Fri'=>'7:30am9pm','Sat'=>'7:30am9pm','Sun'=>'9am8pm']),
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'=>'8am6pm','Tue'=>'8am6pm','Wed'=>'8am6pm','Thu'=>'8am6pm','Fri'=>'8am6pm','Sat'=>'8am5pm','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:30am9pm','Tue'=>'7:30am9pm','Wed'=>'7:30am9pm','Thu'=>'7:30am9pm','Fri'=>'7:30am9pm','Sat'=>'7:30am9pm','Sun'=>'9am8pm']),
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'=>'8am9pm','Tue'=>'8am9pm','Wed'=>'8am9pm','Thu'=>'8am9pm','Fri'=>'8am9pm','Sat'=>'9am6pm','Sun'=>'10am6pm']),
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'=>'10am7pm','Tue'=>'10am7pm','Wed'=>'10am7pm','Thu'=>'10am7pm','Fri'=>'10am7pm','Sat'=>'10am7pm','Sun'=>'12pm5pm']),
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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am6pm','Sat'=>'9am3pm','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:30am5:30pm','Tue'=>'7:30am5:30pm','Wed'=>'7:30am5:30pm','Thu'=>'7:30am5:30pm','Fri'=>'7:30am5:30pm','Sat'=>'8am3pm','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'=>'10am5pm','Tue'=>'10am5pm','Wed'=>'10am5pm','Thu'=>'10am5pm','Fri'=>'10am6pm','Sat'=>'10am4pm','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'=>'10am5pm','Tue'=>'10am5pm','Wed'=>'10am5pm','Thu'=>'10am5pm','Fri'=>'10am5pm','Sat'=>'10am4pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am6pm','Sat'=>'9am5pm','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'=>'9am5:30pm','Tue'=>'9am5:30pm','Wed'=>'9am5:30pm','Thu'=>'9am5:30pm','Fri'=>'9am6pm','Sat'=>'9am5pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5pm','Sat'=>'10am3pm','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'=>'10am8pm','Tue'=>'10am8pm','Wed'=>'10am8pm','Thu'=>'10am8pm','Fri'=>'10am9pm','Sat'=>'10am9pm','Sun'=>'12pm7pm']),
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'=>'9am6pm','Tue'=>'9am6pm','Wed'=>'9am6pm','Thu'=>'9am6pm','Fri'=>'9am7pm','Sat'=>'8am7pm','Sun'=>'10am4pm']),
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'=>'10am5pm','Tue'=>'10am5pm','Wed'=>'10am5pm','Thu'=>'10am5pm','Fri'=>'10am5pm','Sat'=>'10am4pm','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'=>'8am5pm','Tue'=>'8am5pm','Wed'=>'8am5pm','Thu'=>'8am5pm','Fri'=>'8am5pm','Sat'=>'8am12pm','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:30am5pm','Tue'=>'7:30am5pm','Wed'=>'7:30am5pm','Thu'=>'7:30am5pm','Fri'=>'7:30am5pm','Sat'=>'8am12pm','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'=>'9am7pm','Tue'=>'9am7pm','Wed'=>'9am7pm','Thu'=>'9am7pm','Fri'=>'9am7pm','Sat'=>'9am5pm','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'=>'9am6pm','Tue'=>'9am6pm','Wed'=>'9am6pm','Thu'=>'9am6pm','Fri'=>'9am6pm','Sat'=>'9am1pm','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'=>'10am7pm','Tue'=>'10am7pm','Wed'=>'10am7pm','Thu'=>'10am7pm','Fri'=>'10am5pm','Sat'=>'10am4pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5:30pm','Sat'=>'9am12pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am6pm','Sat'=>'9am12pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5:30pm','Sat'=>'9am12pm','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'=>'9am6pm','Tue'=>'9am6pm','Wed'=>'9am6pm','Thu'=>'9am6pm','Fri'=>'9am6pm','Sat'=>'9am5pm','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'=>'9am7pm','Tue'=>'9am7pm','Wed'=>'9am7pm','Thu'=>'9am7pm','Fri'=>'9am7pm','Sat'=>'9am6pm','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'=>'9am7pm','Tue'=>'9am7pm','Wed'=>'9am7pm','Thu'=>'9am7pm','Fri'=>'9am7pm','Sat'=>'9am5pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5pm','Sat'=>'9am3pm','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'=>'8am5pm','Tue'=>'8am5pm','Wed'=>'8am5pm','Thu'=>'8am5pm','Fri'=>'8am5pm','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'=>'8am5:30pm','Tue'=>'8am5:30pm','Wed'=>'8am5:30pm','Thu'=>'8am5:30pm','Fri'=>'8am5:30pm','Sat'=>'8am12pm','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'=>'9am6pm','Tue'=>'9am6pm','Wed'=>'9am6pm','Thu'=>'9am6pm','Fri'=>'9am6pm','Sat'=>'10am4pm','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:30am5pm','Tue'=>'7:30am5pm','Wed'=>'7:30am5pm','Thu'=>'7:30am5pm','Fri'=>'7:30am5pm','Sat'=>'8am12pm','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'=>'10am6pm','Thu'=>'10am6pm','Fri'=>'9am7pm','Sat'=>'8am7pm','Sun'=>'8am5pm']),
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'=>'6am9pm','Tue'=>'6am9pm','Wed'=>'6am9pm','Thu'=>'6am9pm','Fri'=>'6am8pm','Sat'=>'8am6pm','Sun'=>'12pm6pm']),
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'=>'DawnDusk','Tue'=>'DawnDusk','Wed'=>'DawnDusk','Thu'=>'DawnDusk','Fri'=>'DawnDusk','Sat'=>'DawnDusk','Sun'=>'DawnDusk']),
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'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am6pm','Sat'=>'8am4pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5pm','Sat'=>'8am4pm','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'=>'11am7pm','Tue'=>'11am7pm','Wed'=>'11am7pm','Thu'=>'11am7pm','Fri'=>'11am8pm','Sat'=>'10am6pm','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'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5pm','Sat'=>'8am3pm','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:30am4:30pm','Tue'=>'8:30am4:30pm','Wed'=>'8:30am4:30pm','Thu'=>'8:30am4:30pm','Fri'=>'8:30am4: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:30am4:30pm','Tue'=>'8:30am4:30pm','Wed'=>'8:30am4:30pm','Thu'=>'8:30am4:30pm','Fri'=>'8:30am4: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'=>'9am4pm','Tue'=>'9am4pm','Wed'=>'9am4pm','Thu'=>'9am4pm','Fri'=>'9am4pm','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. WednesdaySunday, 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 9pm1am 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 MonFri 8am8pm and weekends 9am5pm. 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 MonFri 8:30am4: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);
}
+107
View File
@@ -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" : "");
}
+107
View File
@@ -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" : "");
}
+5
View File
@@ -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
View File
@@ -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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5pm','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'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am9pm','Sat'=>'11am9pm','Sun'=>'12pm9pm']),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'=>'5pm9pm','Wed'=>'5pm9pm','Thu'=>'5pm9pm','Fri'=>'5pm10pm','Sat'=>'4pm10pm','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'=>'4pm12am','Tue'=>'4pm12am','Wed'=>'4pm12am','Thu'=>'4pm12am','Fri'=>'4pm2am','Sat'=>'12pm2am','Sun'=>'12pm10pm']),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'=>'4pm11pm','Wed'=>'4pm11pm','Thu'=>'4pm11pm','Fri'=>'3pm1am','Sat'=>'12pm1am','Sun'=>'12pm8pm']),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'=>'7am3pm','Tue'=>'7am5pm','Wed'=>'7am5pm','Thu'=>'7am5pm','Fri'=>'7am5pm','Sat'=>'8am5pm','Sun'=>'9am2pm']),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'=>'6am8pm','Tue'=>'6am8pm','Wed'=>'6am8pm','Thu'=>'6am8pm','Fri'=>'6am9pm','Sat'=>'6am9pm','Sun'=>'7am3pm']),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'=>'11am9pm','Tue'=>'11am9pm','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am10pm','Sat'=>'11am10pm','Sun'=>'12pm9pm']),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'=>'6am11pm','Tue'=>'6am11pm','Wed'=>'6am11pm','Thu'=>'6am11pm','Fri'=>'6am12am','Sat'=>'6am12am','Sun'=>'7am11pm']),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'=>'6am12am','Tue'=>'6am12am','Wed'=>'6am12am','Thu'=>'6am12am','Fri'=>'6am1am','Sat'=>'6am1am','Sun'=>'7am12am']),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:30am12am','Tue'=>'10:30am12am','Wed'=>'10:30am12am','Thu'=>'10:30am12am','Fri'=>'10:30am1am','Sat'=>'10:30am1am','Sun'=>'10:30am12am']),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'=>'10am10pm','Tue'=>'10am10pm','Wed'=>'10am10pm','Thu'=>'10am10pm','Fri'=>'10am10:30pm','Sat'=>'10am10:30pm','Sun'=>'11am9pm']),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'=>'11am10pm','Tue'=>'11am10pm','Wed'=>'11am10pm','Thu'=>'11am10pm','Fri'=>'11am11pm','Sat'=>'11am11pm','Sun'=>'12pm9pm']),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'=>'11am9pm','Tue'=>'11am9pm','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am10pm','Sat'=>'11am10pm','Sun'=>'12pm9pm']),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'=>'7am10pm','Tue'=>'7am10pm','Wed'=>'7am10pm','Thu'=>'7am10pm','Fri'=>'7am10pm','Sat'=>'8am10pm','Sun'=>'9am9pm']),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'=>'7am12am','Tue'=>'7am12am','Wed'=>'7am12am','Thu'=>'7am12am','Fri'=>'7am2am','Sat'=>'7am2am','Sun'=>'8am12am']),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'=>'11am11pm','Tue'=>'11am11pm','Wed'=>'11am11pm','Thu'=>'11am11pm','Fri'=>'11am1am','Sat'=>'11am1am','Sun'=>'12pm9pm']),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'=>'10am6pm','Tue'=>'10am6pm','Wed'=>'10am6pm','Thu'=>'10am6pm','Fri'=>'10am7pm','Sat'=>'10am7pm','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'=>'11am9pm','Tue'=>'11am9pm','Wed'=>'11am9pm','Thu'=>'11am9pm','Fri'=>'11am10pm','Sat'=>'11am10pm','Sun'=>'12pm9pm']),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'=>'8am6pm','Tue'=>'8am6pm','Wed'=>'8am6pm','Thu'=>'8am6pm','Fri'=>'8am6pm','Sat'=>'8am5pm','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'=>'8am9pm','Tue'=>'8am9pm','Wed'=>'8am9pm','Thu'=>'8am9pm','Fri'=>'8am9pm','Sat'=>'8am9pm','Sun'=>'9am7pm']),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:30am9pm','Tue'=>'7:30am9pm','Wed'=>'7:30am9pm','Thu'=>'7:30am9pm','Fri'=>'7:30am9pm','Sat'=>'7:30am9pm','Sun'=>'9am8pm']),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'=>'8am9pm','Tue'=>'8am9pm','Wed'=>'8am9pm','Thu'=>'8am9pm','Fri'=>'8am9pm','Sat'=>'9am6pm','Sun'=>'10am6pm']),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'=>'10am7pm','Tue'=>'10am7pm','Wed'=>'10am7pm','Thu'=>'10am7pm','Fri'=>'10am7pm','Sat'=>'10am7pm','Sun'=>'12pm5pm']),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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am6pm','Sat'=>'9am3pm','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:30am5:30pm','Tue'=>'7:30am5:30pm','Wed'=>'7:30am5:30pm','Thu'=>'7:30am5:30pm','Fri'=>'7:30am5:30pm','Sat'=>'8am3pm','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'=>'10am5pm','Tue'=>'10am5pm','Wed'=>'10am5pm','Thu'=>'10am5pm','Fri'=>'10am6pm','Sat'=>'10am4pm','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'=>'10am5pm','Tue'=>'10am5pm','Wed'=>'10am5pm','Thu'=>'10am5pm','Fri'=>'10am5pm','Sat'=>'10am4pm','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'=>'9am6pm','Tue'=>'9am6pm','Wed'=>'9am6pm','Thu'=>'9am6pm','Fri'=>'9am7pm','Sat'=>'8am7pm','Sun'=>'10am4pm']),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'=>'9am6pm','Tue'=>'9am6pm','Wed'=>'9am6pm','Thu'=>'9am6pm','Fri'=>'9am6pm','Sat'=>'9am1pm','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'=>'10am7pm','Tue'=>'10am7pm','Wed'=>'10am7pm','Thu'=>'10am7pm','Fri'=>'10am5pm','Sat'=>'10am4pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am5:30pm','Sat'=>'9am12pm','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'=>'9am5pm','Tue'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am6pm','Sat'=>'9am12pm','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'=>'8am5pm','Tue'=>'8am5pm','Wed'=>'8am5pm','Thu'=>'8am5pm','Fri'=>'8am5pm','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'=>'8am5:30pm','Tue'=>'8am5:30pm','Wed'=>'8am5:30pm','Thu'=>'8am5:30pm','Fri'=>'8am5:30pm','Sat'=>'8am12pm','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:30am5pm','Tue'=>'7:30am5pm','Wed'=>'7:30am5pm','Thu'=>'7:30am5pm','Fri'=>'7:30am5pm','Sat'=>'8am12pm','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'=>'10am6pm','Thu'=>'10am6pm','Fri'=>'9am7pm','Sat'=>'8am7pm','Sun'=>'8am5pm']),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'=>'6am9pm','Tue'=>'6am9pm','Wed'=>'6am9pm','Thu'=>'6am9pm','Fri'=>'6am8pm','Sat'=>'8am6pm','Sun'=>'12pm6pm']),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'=>'9am5pm','Wed'=>'9am5pm','Thu'=>'9am5pm','Fri'=>'9am6pm','Sat'=>'8am4pm','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'=>'11am7pm','Tue'=>'11am7pm','Wed'=>'11am7pm','Thu'=>'11am7pm','Fri'=>'11am8pm','Sat'=>'10am6pm','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:30am4:30pm','Tue'=>'8:30am4:30pm','Wed'=>'8:30am4:30pm','Thu'=>'8:30am4:30pm','Fri'=>'8:30am4: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:30am4:30pm','Tue'=>'8:30am4:30pm','Wed'=>'8:30am4:30pm','Thu'=>'8:30am4:30pm','Fri'=>'8:30am4: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'=>'9am4pm','Tue'=>'9am4pm','Wed'=>'9am4pm','Thu'=>'9am4pm','Fri'=>'9am4pm','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);
}
+49
View File
@@ -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> MonFri 9am4pm</p>
</address>
</div>
</div>
<div class="footer-bar">
<p>&copy; <?=date('Y')?> My Keyser, Discover Keyser, West Virginia — Almost Heaven &bull; Mountain State</p>
</div>
</footer>
<script src="/assets/js/app.js"></script>
<?=$footExtra??''?>
</body>
</html>
+137
View File
@@ -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" : "");
}
+62
View File
@@ -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">