25327e3302
[events.php](/Users/tyemeclifford/Documents/GH/MyKeyser/events.php) and [admin/events.php](/Users/tyemeclifford/Documents/GH/MyKeyser/admin/events.php): event image uploads. [menu.php](/Users/tyemeclifford/Documents/GH/MyKeyser/menu.php), [admin/menus.php](/Users/tyemeclifford/Documents/GH/MyKeyser/admin/menus.php), and [business.php](/Users/tyemeclifford/Documents/GH/MyKeyser/business.php): one configurable menu item image slot. [account.php](/Users/tyemeclifford/Documents/GH/MyKeyser/account.php): circular avatar upload and crop controls. [forum.php](/Users/tyemeclifford/Documents/GH/MyKeyser/forum.php), [admin/forum.php](/Users/tyemeclifford/Documents/GH/MyKeyser/admin/forum.php), [.htaccess](/Users/tyemeclifford/Documents/GH/MyKeyser/.htaccess), and [forum/index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/forum/index.php): forum, categories, topic/reply uploads, permalinks, toggle/moderation. [includes/db.php](/Users/tyemeclifford/Documents/GH/MyKeyser/includes/db.php) and [includes/functions.php](/Users/tyemeclifford/Documents/GH/MyKeyser/includes/functions.php): schema upgrades and shared upload/image helpers. [assets/css/style.css](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/css/style.css): neon styling for forum, images, avatars, attachments.
730 lines
52 KiB
PHP
730 lines
52 KiB
PHP
<?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 app upgrade tables/columns exist (for existing DBs)
|
||
_ensureAppUpgrades($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 upgraded tables/columns exist on old DBs ─── */
|
||
function _hasColumn(PDO $db, string $table, string $column): bool {
|
||
$cols = $db->query("PRAGMA table_info($table)")->fetchAll(PDO::FETCH_ASSOC);
|
||
foreach ($cols as $col) {
|
||
if (($col['name'] ?? '') === $column) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function _ensureAppUpgrades(PDO $db): void {
|
||
if (!_hasColumn($db, 'users', 'email_verified_at')) {
|
||
$db->exec("ALTER TABLE users ADD COLUMN email_verified_at TEXT");
|
||
$db->exec("UPDATE users SET email_verified_at=datetime('now') WHERE is_active=1");
|
||
}
|
||
if (!_hasColumn($db, 'users', 'member_type')) {
|
||
$db->exec("ALTER TABLE users ADD COLUMN member_type TEXT NOT NULL DEFAULT 'member'");
|
||
}
|
||
if (!_hasColumn($db, 'users', 'avatar_path')) {
|
||
$db->exec("ALTER TABLE users ADD COLUMN avatar_path TEXT NOT NULL DEFAULT ''");
|
||
}
|
||
if (!_hasColumn($db, 'businesses', 'video_url')) {
|
||
$db->exec("ALTER TABLE businesses ADD COLUMN video_url TEXT NOT NULL DEFAULT ''");
|
||
}
|
||
if (!_hasColumn($db, 'events', 'image_path')) {
|
||
$db->exec("ALTER TABLE events ADD COLUMN image_path TEXT NOT NULL DEFAULT ''");
|
||
}
|
||
if (!_hasColumn($db, 'menu_items', 'image_path')) {
|
||
$db->exec("ALTER TABLE menu_items ADD COLUMN image_path TEXT NOT NULL DEFAULT ''");
|
||
}
|
||
|
||
$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
|
||
);
|
||
CREATE TABLE IF NOT EXISTS business_claim_requests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
contact_phone TEXT NOT NULL DEFAULT '',
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
admin_notes TEXT NOT NULL DEFAULT '',
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
reviewed_at TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_claim_requests_status ON business_claim_requests(status, created_at);
|
||
CREATE TABLE IF NOT EXISTS email_verification_tokens (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
token_hash TEXT NOT NULL UNIQUE,
|
||
purpose TEXT NOT NULL DEFAULT 'email_verify',
|
||
expires_at TEXT NOT NULL,
|
||
used_at TEXT,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_email_tokens_user ON email_verification_tokens(user_id, purpose, used_at);
|
||
CREATE TABLE IF NOT EXISTS forum_categories (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL UNIQUE,
|
||
slug TEXT NOT NULL UNIQUE,
|
||
description TEXT NOT NULL DEFAULT '',
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS forum_topics (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
category_id INTEGER NOT NULL REFERENCES forum_categories(id) ON DELETE RESTRICT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
slug TEXT NOT NULL UNIQUE,
|
||
body TEXT NOT NULL DEFAULT '',
|
||
is_locked INTEGER NOT NULL DEFAULT 0,
|
||
is_pinned INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS forum_replies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
topic_id INTEGER NOT NULL REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
body TEXT NOT NULL DEFAULT '',
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS forum_attachments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
topic_id INTEGER REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||
reply_id INTEGER REFERENCES forum_replies(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
file_path TEXT NOT NULL,
|
||
original_name TEXT NOT NULL DEFAULT '',
|
||
mime_type TEXT NOT NULL DEFAULT '',
|
||
file_size INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
CHECK (topic_id IS NOT NULL OR reply_id IS NOT NULL)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_forum_topics_category ON forum_topics(category_id, updated_at);
|
||
CREATE INDEX IF NOT EXISTS idx_forum_replies_topic ON forum_replies(topic_id, created_at);
|
||
CREATE INDEX IF NOT EXISTS idx_forum_attachments_topic ON forum_attachments(topic_id);
|
||
CREATE INDEX IF NOT EXISTS idx_forum_attachments_reply ON forum_attachments(reply_id);
|
||
");
|
||
|
||
$defaults = [
|
||
'home_random_interval' => '2hours',
|
||
'site_base_url' => '',
|
||
'mailgun_region' => 'us',
|
||
'mailgun_domain' => '',
|
||
'mailgun_api_key' => '',
|
||
'mailgun_from_name' => 'Discover Keyser WV',
|
||
'mailgun_from_email' => '',
|
||
'mailgun_send_mode' => 'html',
|
||
'event_upload_max_mb' => '5',
|
||
'menu_item_image_limit' => '1',
|
||
'menu_upload_max_mb' => '5',
|
||
'avatar_upload_max_mb' => '3',
|
||
'forum_enabled' => '1',
|
||
'forum_upload_max_mb' => '5',
|
||
'forum_allowed_filetypes' => 'jpg,jpeg,png,gif,webp,pdf,txt,doc,docx',
|
||
'forum_image_resize_threshold_mb' => '1',
|
||
'forum_image_max_width' => '1600',
|
||
'forum_image_quality' => '82',
|
||
];
|
||
$stmt = $db->prepare("INSERT OR IGNORE INTO settings(key,value)VALUES(?,?)");
|
||
foreach ($defaults as $k => $v) $stmt->execute([$k, $v]);
|
||
|
||
$catCount = (int)$db->query("SELECT COUNT(*) FROM forum_categories")->fetchColumn();
|
||
if ($catCount === 0) {
|
||
$stmt = $db->prepare("INSERT INTO forum_categories(name,slug,description,sort_order,is_active)VALUES(?,?,?,?,1)");
|
||
foreach ([
|
||
['General', 'general', 'Neighborhood conversation, questions, and introductions.', 1],
|
||
['Local Questions', 'local-questions', 'Ask for Keyser recommendations, resources, and local tips.', 2],
|
||
['Business Talk', 'business-talk', 'Business owner updates, ideas, and community feedback.', 3],
|
||
['Events', 'events', 'Discuss upcoming events and things happening around town.', 4],
|
||
] as $cat) $stmt->execute($cat);
|
||
}
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
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,
|
||
email_verified_at TEXT,
|
||
member_type TEXT NOT NULL DEFAULT 'member',
|
||
avatar_path TEXT NOT NULL DEFAULT '',
|
||
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 '',
|
||
video_url TEXT NOT NULL DEFAULT '',
|
||
hours TEXT NOT NULL DEFAULT '{}',
|
||
lat REAL NOT NULL DEFAULT 39.4364,
|
||
lng REAL NOT NULL DEFAULT -78.9762,
|
||
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 business_claim_requests(
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
contact_phone TEXT NOT NULL DEFAULT '',
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
admin_notes TEXT NOT NULL DEFAULT '',
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
reviewed_at TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS email_verification_tokens(
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
token_hash TEXT NOT NULL UNIQUE,
|
||
purpose TEXT NOT NULL DEFAULT 'email_verify',
|
||
expires_at TEXT NOT NULL,
|
||
used_at TEXT,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
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,
|
||
image_path TEXT NOT NULL DEFAULT '',
|
||
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 '',
|
||
image_path 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'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS forum_categories (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL UNIQUE,
|
||
slug TEXT NOT NULL UNIQUE,
|
||
description TEXT NOT NULL DEFAULT '',
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS forum_topics (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
category_id INTEGER NOT NULL REFERENCES forum_categories(id) ON DELETE RESTRICT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL,
|
||
slug TEXT NOT NULL UNIQUE,
|
||
body TEXT NOT NULL DEFAULT '',
|
||
is_locked INTEGER NOT NULL DEFAULT 0,
|
||
is_pinned INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS forum_replies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
topic_id INTEGER NOT NULL REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
body TEXT NOT NULL DEFAULT '',
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS forum_attachments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
topic_id INTEGER REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||
reply_id INTEGER REFERENCES forum_replies(id) ON DELETE CASCADE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
file_path TEXT NOT NULL,
|
||
original_name TEXT NOT NULL DEFAULT '',
|
||
mime_type TEXT NOT NULL DEFAULT '',
|
||
file_size INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||
CHECK (topic_id IS NOT NULL OR reply_id IS NOT NULL)
|
||
);
|
||
");
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════
|
||
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',
|
||
'home_random_interval'=>'2hours',
|
||
'site_base_url'=>'',
|
||
'mailgun_region'=>'us',
|
||
'mailgun_domain'=>'',
|
||
'mailgun_api_key'=>'',
|
||
'mailgun_from_name'=>'Discover Keyser WV',
|
||
'mailgun_from_email'=>'',
|
||
'mailgun_send_mode'=>'html',
|
||
'event_upload_max_mb'=>'5',
|
||
'menu_item_image_limit'=>'1',
|
||
'menu_upload_max_mb'=>'5',
|
||
'avatar_upload_max_mb'=>'3',
|
||
'forum_enabled'=>'1',
|
||
'forum_upload_max_mb'=>'5',
|
||
'forum_allowed_filetypes'=>'jpg,jpeg,png,gif,webp,pdf,txt,doc,docx',
|
||
'forum_image_resize_threshold_mb'=>'1',
|
||
'forum_image_max_width'=>'1600',
|
||
'forum_image_quality'=>'82'] 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,email_verified_at,member_type)
|
||
VALUES('administrator','admin@discoverkeyser.com',?,1,datetime('now'),'member')",[$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);
|
||
|
||
foreach ([
|
||
['General', 'general', 'Neighborhood conversation, questions, and introductions.', 1],
|
||
['Local Questions', 'local-questions', 'Ask for Keyser recommendations, resources, and local tips.', 2],
|
||
['Business Talk', 'business-talk', 'Business owner updates, ideas, and community feedback.', 3],
|
||
['Events', 'events', 'Discuss upcoming events and things happening around town.', 4],
|
||
] as $fc) qrun("INSERT OR IGNORE INTO forum_categories(name,slug,description,sort_order,is_active)VALUES(?,?,?,?,1)", $fc);
|
||
|
||
$cid = fn(string $n): int => (int)qval("SELECT id FROM categories WHERE name=?",[$n]);
|
||
$h = fn(array $d): string => json_encode($d);
|
||
$std = $h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'Closed','Sun'=>'Closed']);
|
||
$always= $h(['Mon'=>'Open 24 hrs','Tue'=>'Open 24 hrs','Wed'=>'Open 24 hrs','Thu'=>'Open 24 hrs','Fri'=>'Open 24 hrs','Sat'=>'Open 24 hrs','Sun'=>'Open 24 hrs']);
|
||
|
||
$D=$cid('Dining & Food'); $SH=$cid('Shopping & Retail'); $LO=$cid('Lodging');
|
||
$HC=$cid('Healthcare'); $ED=$cid('Education'); $FI=$cid('Financial Services');
|
||
$AU=$cid('Automotive'); $PS=$cid('Professional Services'); $RF=$cid('Recreation & Fitness');
|
||
$BP=$cid('Beauty & Personal Care'); $GV=$cid('Government & Civic');
|
||
$LG=$cid('Legal Services'); $CH=$cid('Churches & Faith');
|
||
|
||
$bi = $db->prepare("INSERT INTO businesses(name,slug,category_id,subcategory,description,address,phone,email,website,hours,lat,lng,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||
|
||
$biz = [
|
||
["Castiglia's Italian Eatery",'castiglia-italian',$D,'Italian Restaurant',
|
||
"Keyser's most beloved restaurant. Family recipes passed down for generations: handmade pasta, wood-fired dishes, classic antipasti, and decadent Italian desserts. Known for huge servings and amazing food.",
|
||
'401 S. Mineral St., Keyser, WV 26726','304-788-1300','','',
|
||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–9pm','Sat'=>'11am–9pm','Sun'=>'12pm–9pm']),39.4340,-78.9734,1],
|
||
['The Candlewyck Inn','candlewyck-inn',$D,'Fine Dining',
|
||
"Elegant fine dining in a beautifully restored historic setting. Upscale American cuisine with locally sourced seasonal ingredients. Award-winning wine list. Keyser's premier destination for special occasions.",
|
||
'65 S. Mineral St., Keyser, WV 26726','304-788-6594','','',
|
||
$h(['Mon'=>'Closed','Tue'=>'5pm–9pm','Wed'=>'5pm–9pm','Thu'=>'5pm–9pm','Fri'=>'5pm–10pm','Sat'=>'4pm–10pm','Sun'=>'Closed']),39.4370,-78.9734,1],
|
||
["Clancy's Irish Pub",'clancys-irish-pub',$D,'Irish Pub & Bar',
|
||
"Keyser's favorite neighborhood pub! Cold craft beers on tap, hearty pub fare, sports on every screen, trivia nights on Wednesdays, and live music on weekends.",
|
||
'777 Armstrong St., Keyser, WV 26726','304-788-1133','','',
|
||
$h(['Mon'=>'4pm–12am','Tue'=>'4pm–12am','Wed'=>'4pm–12am','Thu'=>'4pm–12am','Fri'=>'4pm–2am','Sat'=>'12pm–2am','Sun'=>'12pm–10pm']),39.4390,-78.9750,1],
|
||
['North Branch Craft Pub','north-branch-craft-pub',$D,'Craft Brewery & Pub',
|
||
"Keyser's craft beer destination. Local and regional craft beers on rotating taps, great pub fare, and stunning North Branch river views. Live entertainment regularly.",
|
||
'101 Armstrong St., Ste. 1, Keyser, WV 26726','304-790-7125','','',
|
||
$h(['Mon'=>'Closed','Tue'=>'4pm–11pm','Wed'=>'4pm–11pm','Thu'=>'4pm–11pm','Fri'=>'3pm–1am','Sat'=>'12pm–1am','Sun'=>'12pm–8pm']),39.4392,-78.9750,1],
|
||
['Queens Point Coffee','queens-point-coffee',$D,'Coffee Shop',
|
||
'"THE best coffee shop I\'ve ever been to." Named after the iconic Queens Point cliff. Iced matcha, lattes, specialty drinks, amazing food, and friendly staff.',
|
||
'101 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||
$h(['Mon'=>'7am–3pm','Tue'=>'7am–5pm','Wed'=>'7am–5pm','Thu'=>'7am–5pm','Fri'=>'7am–5pm','Sat'=>'8am–5pm','Sun'=>'9am–2pm']),39.4392,-78.9750,1],
|
||
['Royal Restaurant','royal-restaurant',$D,'American / Comfort Food',
|
||
"A Keyser classic for generations. Hearty American comfort food for breakfast, lunch, and dinner. Known for great breakfasts, homemade pies, and friendly small-town service.",
|
||
'88 N. Main St., Keyser, WV 26726','304-788-9825','','',
|
||
$h(['Mon'=>'6am–8pm','Tue'=>'6am–8pm','Wed'=>'6am–8pm','Thu'=>'6am–8pm','Fri'=>'6am–9pm','Sat'=>'6am–9pm','Sun'=>'7am–3pm']),39.4378,-78.9734,0],
|
||
['Fat Bottom Grille','fat-bottom-grille',$D,'American Bar & Grill',
|
||
'"THE best place in town to get a burger or steak sub." Everything made fresh. The smash burger is juicy and flavored to perfection. Huge portions at excellent prices.',
|
||
'Keyser, WV 26726','304-788-0000','','',
|
||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),39.4360,-78.9740,1],
|
||
["Denny's",'dennys',$D,'American / Diner',
|
||
"Open 24/7. America's diner at Route 220 South. Grand Slams, hearty burgers, and all-day breakfast.",
|
||
'825 S. Mineral St. (US Rt. 220 S), Keyser, WV 26726','304-788-3090','','dennys.com',$always,39.4300,-78.9730,0],
|
||
["McDonald's",'mcdonalds',$D,'Fast Food',
|
||
"Burgers, fries, breakfast sandwiches, and McCafé beverages. Drive-through available.",
|
||
'700 S. Mineral St., Keyser, WV 26726','304-788-0604','','mcdonalds.com',
|
||
$h(['Mon'=>'6am–11pm','Tue'=>'6am–11pm','Wed'=>'6am–11pm','Thu'=>'6am–11pm','Fri'=>'6am–12am','Sat'=>'6am–12am','Sun'=>'7am–11pm']),39.4310,-78.9730,0],
|
||
['Burger King','burger-king',$D,'Fast Food',
|
||
'Home of the Whopper! Flame-grilled burgers and crispy fries. Drive-through available.',
|
||
'RR#3 Box 3240, New Creek Hwy, Keyser, WV 26726','304-788-0000','','burgerking.com',
|
||
$h(['Mon'=>'6am–12am','Tue'=>'6am–12am','Wed'=>'6am–12am','Thu'=>'6am–12am','Fri'=>'6am–1am','Sat'=>'6am–1am','Sun'=>'7am–12am']),39.4285,-78.9720,0],
|
||
["Domino's Pizza",'dominos',$D,'Pizza / Delivery',
|
||
'Hot fresh pizza fast. Carryout or delivered. Online ordering at dominos.com.',
|
||
'590 S. Mineral St., Keyser, WV 26726','304-788-6400','','dominos.com',
|
||
$h(['Mon'=>'10:30am–12am','Tue'=>'10:30am–12am','Wed'=>'10:30am–12am','Thu'=>'10:30am–12am','Fri'=>'10:30am–1am','Sat'=>'10:30am–1am','Sun'=>'10:30am–12am']),39.4330,-78.9730,0],
|
||
['Dairy Queen Grill & Chill','dairy-queen',$D,'Ice Cream / Fast Food',
|
||
"Signature Blizzard® treats, soft-serve cones, and grill burgers. A Keyser summer tradition!",
|
||
'460 S. Mineral St., Keyser, WV 26726','304-788-1499','','dairyqueen.com',
|
||
$h(['Mon'=>'10am–10pm','Tue'=>'10am–10pm','Wed'=>'10am–10pm','Thu'=>'10am–10pm','Fri'=>'10am–10:30pm','Sat'=>'10am–10:30pm','Sun'=>'11am–9pm']),39.4320,-78.9730,0],
|
||
["Fox's Pizza Den",'foxs-pizza',$D,'Pizza',
|
||
"Local pizza legend! Hand-tossed pizzas, subs, wings, and salads. A true Keyser staple.",
|
||
'567 S. Mineral St., Keyser, WV 26726','304-788-1149','','',
|
||
$h(['Mon'=>'11am–10pm','Tue'=>'11am–10pm','Wed'=>'11am–10pm','Thu'=>'11am–10pm','Fri'=>'11am–11pm','Sat'=>'11am–11pm','Sun'=>'12pm–9pm']),39.4315,-78.9730,0],
|
||
['Little Caesars','little-caesars',$D,'Pizza / Fast Food',
|
||
'Hot-N-Ready pizzas at unbeatable prices. No wait. Crazy Bread and Italian favorites.',
|
||
'30 Armstrong St., Keyser, WV 26726','304-788-7738','','littlecaesars.com',
|
||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),39.4395,-78.9750,0],
|
||
['Subway','subway',$D,'Sandwiches / Fast Food',
|
||
'Fresh-built footlong subs, wraps, and salads.',
|
||
'2 Heskiet St. (Gulf), Keyser, WV 26726','304-788-3613','','subway.com',
|
||
$h(['Mon'=>'7am–10pm','Tue'=>'7am–10pm','Wed'=>'7am–10pm','Thu'=>'7am–10pm','Fri'=>'7am–10pm','Sat'=>'8am–10pm','Sun'=>'9am–9pm']),39.4260,-78.9720,0],
|
||
['Taco Bell','taco-bell',$D,'Mexican Fast Food',
|
||
'Tacos, burritos, nachos, and Crunchwrap Supremes. Drive-through available.',
|
||
'41 Plaza Drive, Keyser, WV 26726','304-788-0000','','tacobell.com',
|
||
$h(['Mon'=>'7am–12am','Tue'=>'7am–12am','Wed'=>'7am–12am','Thu'=>'7am–12am','Fri'=>'7am–2am','Sat'=>'7am–2am','Sun'=>'8am–12am']),39.4265,-78.9722,0],
|
||
["Ducky's Bar & Grill",'duckys-bar-grill',$D,'American Bar & Grill',
|
||
'"The ultimate combination of bar and grill." Famous Tennessee whiskey burger and chili cheese fries.',
|
||
'Keyser, WV 26726','304-788-0000','','',
|
||
$h(['Mon'=>'11am–11pm','Tue'=>'11am–11pm','Wed'=>'11am–11pm','Thu'=>'11am–11pm','Fri'=>'11am–1am','Sat'=>'11am–1am','Sun'=>'12pm–9pm']),39.4358,-78.9738,0],
|
||
["Martie's Hot Dog Stand",'marties-hot-dogs',$D,'Hot Dogs / Street Food',
|
||
"A beloved Keyser institution. Classic hot dogs with all the fixings for generations.",
|
||
'96 N. Main St., Keyser, WV 26726','304-788-7690','','',
|
||
$h(['Mon'=>'10am–6pm','Tue'=>'10am–6pm','Wed'=>'10am–6pm','Thu'=>'10am–6pm','Fri'=>'10am–7pm','Sat'=>'10am–7pm','Sun'=>'Closed']),39.4375,-78.9734,0],
|
||
["Jin's Asian Cuisine 2",'jins-asian-cuisine',$D,'Asian / Chinese',
|
||
"Orange chicken, bourbon chicken, lo mein, fried rice, and crab rangoon made fresh to order.",
|
||
'196 N. Tornado Way #11, Keyser, WV 26726','304-788-2222','','',
|
||
$h(['Mon'=>'11am–9pm','Tue'=>'11am–9pm','Wed'=>'11am–9pm','Thu'=>'11am–9pm','Fri'=>'11am–10pm','Sat'=>'11am–10pm','Sun'=>'12pm–9pm']),39.4355,-78.9730,0],
|
||
// Shopping
|
||
["Wayne's Country Meats",'waynes-country-meats',$SH,'Butcher / Specialty Meats',
|
||
'Premium quality meats from local farms. Custom cuts, specialty items, fresh jerky.',
|
||
'670 Armstrong St., Keyser, WV 26726','304-788-5956','','',
|
||
$h(['Mon'=>'8am–6pm','Tue'=>'8am–6pm','Wed'=>'8am–6pm','Thu'=>'8am–6pm','Fri'=>'8am–6pm','Sat'=>'8am–5pm','Sun'=>'Closed']),39.4395,-78.9750,1],
|
||
['Save-A-Lot','save-a-lot',$SH,'Grocery',
|
||
'Affordable grocery shopping. Fresh produce, quality meats, dairy, and pantry essentials.',
|
||
'S. Mineral St., Keyser, WV 26726','304-788-7570','','save-a-lot.com',
|
||
$h(['Mon'=>'8am–9pm','Tue'=>'8am–9pm','Wed'=>'8am–9pm','Thu'=>'8am–9pm','Fri'=>'8am–9pm','Sat'=>'8am–9pm','Sun'=>'9am–7pm']),39.4298,-78.9728,0],
|
||
['AutoZone','autozone',$SH,'Auto Parts',
|
||
"America's leading auto parts retailer. Free battery testing, loaner tool program.",
|
||
'2 W. Piedmont St., Keyser, WV 26726','304-788-9058','','autozone.com',
|
||
$h(['Mon'=>'7:30am–9pm','Tue'=>'7:30am–9pm','Wed'=>'7:30am–9pm','Thu'=>'7:30am–9pm','Fri'=>'7:30am–9pm','Sat'=>'7:30am–9pm','Sun'=>'9am–8pm']),39.4378,-78.9734,0],
|
||
['CVS Pharmacy','cvs',$SH,'Pharmacy / Health & Beauty',
|
||
'Prescriptions, health products, beauty supplies, and household essentials.',
|
||
'45 S. Mineral St., Keyser, WV 26726','304-788-3443','','cvs.com',
|
||
$h(['Mon'=>'8am–9pm','Tue'=>'8am–9pm','Wed'=>'8am–9pm','Thu'=>'8am–9pm','Fri'=>'8am–9pm','Sat'=>'9am–6pm','Sun'=>'10am–6pm']),39.4368,-78.9734,0],
|
||
['AT&T Store','att-store',$SH,'Telecommunications',
|
||
'Latest smartphones, tablets, and wireless plans.',
|
||
'862 S. Mineral St., Keyser, WV 26726','304-788-9000','','att.com',
|
||
$h(['Mon'=>'10am–7pm','Tue'=>'10am–7pm','Wed'=>'10am–7pm','Thu'=>'10am–7pm','Fri'=>'10am–7pm','Sat'=>'10am–7pm','Sun'=>'12pm–5pm']),39.4292,-78.9728,0],
|
||
["Christy's Florist",'christys-florist',$SH,'Florist',
|
||
'Fresh floral arrangements for weddings, funerals, anniversaries, and celebrations.',
|
||
'81 N. Main St., Keyser, WV 26726','304-788-3679','','',
|
||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'9am–3pm','Sun'=>'Closed']),39.4382,-78.9734,0],
|
||
['Southern States','southern-states',$SH,'Farm & Garden Supply',
|
||
'Agricultural supplies, pet food, hardware, seed, fertilizer, and home and garden products.',
|
||
'201 Patrick St., Keyser, WV 26726','304-788-2317','','southernstates.com',
|
||
$h(['Mon'=>'7:30am–5:30pm','Tue'=>'7:30am–5:30pm','Wed'=>'7:30am–5:30pm','Thu'=>'7:30am–5:30pm','Fri'=>'7:30am–5:30pm','Sat'=>'8am–3pm','Sun'=>'Closed']),39.4410,-78.9755,0],
|
||
['Back Alley Crafts & Tarts','back-alley-crafts',$SH,'Crafts & Gifts',
|
||
'Unique handmade crafts, locally made artisan goods, and specialty gifts.',
|
||
'226 S. Main St., Keyser, WV 26726','304-813-6256','','',
|
||
$h(['Mon'=>'10am–5pm','Tue'=>'10am–5pm','Wed'=>'10am–5pm','Thu'=>'10am–5pm','Fri'=>'10am–6pm','Sat'=>'10am–4pm','Sun'=>'Closed']),39.4357,-78.9734,0],
|
||
['Heaven Sent Creations','heaven-sent-creations',$SH,'Gifts & Crafts',
|
||
'Inspirational gifts, custom creations, home décor, and unique finds.',
|
||
'117 Armstrong St., Keyser, WV 26726','304-790-7402','','',
|
||
$h(['Mon'=>'10am–5pm','Tue'=>'10am–5pm','Wed'=>'10am–5pm','Thu'=>'10am–5pm','Fri'=>'10am–5pm','Sat'=>'10am–4pm','Sun'=>'Closed']),39.4393,-78.9750,0],
|
||
['Thunder Hill Outfitters','thunder-hill-outfitters',$SH,'Hunting & Fishing Supplies',
|
||
"Hunting and fishing supplies, gear, licenses, and local expertise.",
|
||
'105 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–7pm','Sat'=>'8am–7pm','Sun'=>'10am–4pm']),39.4392,-78.9750,0],
|
||
// Lodging
|
||
['SureStay Plus by Best Western Keyser','surestay-best-western',$LO,'Hotel',
|
||
'Free hot breakfast daily, free WiFi, indoor pool, and fitness center. Pet-friendly.',
|
||
'New Creek Hwy, Keyser, WV 26726','304-788-0000','','bestwestern.com',$always,39.4298,-78.9705,1],
|
||
['Keyser Inn','keyser-inn',$LO,'Motel',
|
||
'Affordable, clean accommodations. Continental breakfast, free WiFi, pet-friendly.',
|
||
'Keyser, WV 26726','304-788-0000','','',$always,39.4360,-78.9734,0],
|
||
// Healthcare
|
||
['WVU Medicine Potomac Valley Hospital','pvh',$HC,'Hospital / Medical Center',
|
||
'Full-service community hospital. Emergency department, surgery, maternity care, cardiac services.',
|
||
'100 Pin Oak Ln., Keyser, WV 26726','304-597-3500','','wvumedicine.org',$always,39.4275,-78.9725,1],
|
||
['Med-a-Save Pharmacy','med-a-save',$HC,'Independent Pharmacy',
|
||
'Local independent pharmacy. Prescriptions, compounding, and immunizations.',
|
||
'818 S. Mineral St., Keyser, WV 26726','304-788-6010','','',
|
||
$h(['Mon'=>'9am–6pm','Tue'=>'9am–6pm','Wed'=>'9am–6pm','Thu'=>'9am–6pm','Fri'=>'9am–6pm','Sat'=>'9am–1pm','Sun'=>'Closed']),39.4287,-78.9728,0],
|
||
// Education
|
||
['Potomac State College of WVU','potomac-state-college',$ED,'Community College / University',
|
||
"WVU Potomac State College — founded 1901 on historic Fort Fuller. Associate and bachelor's degrees in 40+ programs. Home of the Catamounts.",
|
||
'101 Fort Ave., Keyser, WV 26726','304-788-6800','','potomacstatecollege.edu',$std,39.4368,-78.9715,1],
|
||
['Mineral County Public Library','mineral-county-library',$ED,'Public Library',
|
||
'Books, digital resources, internet access, printing, and community programs for all ages.',
|
||
'Keyser, WV 26726','304-788-3100','','',
|
||
$h(['Mon'=>'10am–7pm','Tue'=>'10am–7pm','Wed'=>'10am–7pm','Thu'=>'10am–7pm','Fri'=>'10am–5pm','Sat'=>'10am–4pm','Sun'=>'Closed']),39.4370,-78.9734,0],
|
||
// Financial
|
||
["First United Bank & Trust",'first-united-bank',$FI,'Community Bank',
|
||
'Community banking rooted in West Virginia. Personal checking, savings, mortgages.',
|
||
'29 West Southern Dr., Keyser, WV 26726','304-788-2552','','mybank.com',
|
||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5:30pm','Sat'=>'9am–12pm','Sun'=>'Closed']),39.4372,-78.9740,0],
|
||
['M&T Bank','m-t-bank',$FI,'Bank',
|
||
'Full-service personal, small business, and commercial banking. 24/7 ATM.',
|
||
'67 N. Main St., Keyser, WV 26726','304-788-6782','','mtb.com',
|
||
$h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'9am–12pm','Sun'=>'Closed']),39.4380,-78.9734,0],
|
||
// Automotive
|
||
["Boddy's Automotive",'boddys-auto',$AU,'Full Service Auto Repair',
|
||
'Trusted local auto repair for all makes and models. Honest prices.',
|
||
'220 Armstrong St., Keyser, WV 26726','304-788-5511','','',
|
||
$h(['Mon'=>'8am–5pm','Tue'=>'8am–5pm','Wed'=>'8am–5pm','Thu'=>'8am–5pm','Fri'=>'8am–5pm','Sat'=>'Closed','Sun'=>'Closed']),39.4392,-78.9750,0],
|
||
['Gimme A Brake Auto','gimme-a-brake',$AU,'Brakes / Tires / Oil Changes',
|
||
'Fast, friendly, and affordable auto care. No appointment needed for routine maintenance.',
|
||
'375 West Piedmont St., Keyser, WV 26726','304-788-7810','','',
|
||
$h(['Mon'=>'8am–5:30pm','Tue'=>'8am–5:30pm','Wed'=>'8am–5:30pm','Thu'=>'8am–5:30pm','Fri'=>'8am–5:30pm','Sat'=>'8am–12pm','Sun'=>'Closed']),39.4378,-78.9748,0],
|
||
// Professional Services
|
||
['Boggs Supply and Rental Center','boggs-supply',$PS,'Equipment Rental / Supply',
|
||
'Tools, equipment, and supplies for rent or purchase.',
|
||
'464 Harley O Staggers Sr. Dr., Keyser, WV 26726','304-788-1617','','',
|
||
$h(['Mon'=>'7:30am–5pm','Tue'=>'7:30am–5pm','Wed'=>'7:30am–5pm','Thu'=>'7:30am–5pm','Fri'=>'7:30am–5pm','Sat'=>'8am–12pm','Sun'=>'Closed']),39.4382,-78.9748,0],
|
||
['Legal Aid of WV','legal-aid',$LG,'Legal Aid',
|
||
'Free civil legal aid for eligible low-income West Virginians.',
|
||
'251 W. Piedmont St., Keyser, WV 26726','304-788-6770','','lawv.net',$std,39.4374,-78.9750,0],
|
||
// Recreation
|
||
['North Branch Ventures','north-branch-ventures',$RF,'Kayak Rental / Outdoor Recreation',
|
||
"Kayak and river supply rentals on the beautiful North Branch Potomac. Guided river experiences.",
|
||
'101 Armstrong St., Keyser, WV 26726','304-790-7125','','',
|
||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'10am–6pm','Thu'=>'10am–6pm','Fri'=>'9am–7pm','Sat'=>'8am–7pm','Sun'=>'8am–5pm']),39.4392,-78.9750,1],
|
||
['PSC Recreation Center','psc-rec-center',$RF,'Fitness Center / Pool',
|
||
"Full-service fitness center with indoor pool, hot tub, strength and cardio, group exercise. Open to the public.",
|
||
'J. Edward Kelley Complex, 101 Fort Ave., Keyser, WV','304-788-6800','','potomacstatecollege.edu',
|
||
$h(['Mon'=>'6am–9pm','Tue'=>'6am–9pm','Wed'=>'6am–9pm','Thu'=>'6am–9pm','Fri'=>'6am–8pm','Sat'=>'8am–6pm','Sun'=>'12pm–6pm']),39.4368,-78.9715,0],
|
||
['Mineral County Parks & Recreation','mineral-county-parks',$RF,'Parks & Recreation',
|
||
"Larenim Park — 365 acres with pavilions, 600-seat amphitheater, fishing ponds, and trails.",
|
||
'Keyser, WV 26726','304-788-3066','','mineralwv.gov',
|
||
$h(['Mon'=>'Open Daily','Tue'=>'Open Daily','Wed'=>'Open Daily','Thu'=>'Open Daily','Fri'=>'Open Daily','Sat'=>'Open Daily','Sun'=>'Open Daily']),39.4350,-78.9720,0],
|
||
// Beauty
|
||
['Eclips Hair Salon','eclips',$BP,'Hair Salon',
|
||
'Full-service hair salon. Cuts, color, styling, and treatments for men and women.',
|
||
'153 S. Mineral St., Keyser, WV 26726','304-788-5578','','',
|
||
$h(['Mon'=>'Closed','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–6pm','Sat'=>'8am–4pm','Sun'=>'Closed']),39.4352,-78.9733,0],
|
||
['In The Skin II','in-the-skin',$BP,'Tattoo & Body Art',
|
||
'Professional tattoo and body art studio. Custom designs, cover-ups, and piercings.',
|
||
'129 N. Main St., Keyser, WV 26726','304-597-2070','','',
|
||
$h(['Mon'=>'11am–7pm','Tue'=>'11am–7pm','Wed'=>'11am–7pm','Thu'=>'11am–7pm','Fri'=>'11am–8pm','Sat'=>'10am–6pm','Sun'=>'Closed']),39.4373,-78.9734,0],
|
||
// Government
|
||
['Mineral County Courthouse','mineral-county-courthouse',$GV,'County Government',
|
||
"Historic 1868 courthouse listed on the National Register of Historic Places — the architectural crown jewel of downtown Keyser.",
|
||
'Keyser, WV 26726','304-788-3003','','mineralwv.gov',
|
||
$h(['Mon'=>'8:30am–4:30pm','Tue'=>'8:30am–4:30pm','Wed'=>'8:30am–4:30pm','Thu'=>'8:30am–4:30pm','Fri'=>'8:30am–4:30pm','Sat'=>'Closed','Sun'=>'Closed']),39.4385,-78.9738,0],
|
||
['City of Keyser','city-of-keyser',$GV,'City Government',
|
||
"Official offices of the City of Keyser, WV. City council, mayor's office, utilities, and municipal services.",
|
||
'Keyser, WV 26726','304-788-0222','','cityofkeyser.com',
|
||
$h(['Mon'=>'8:30am–4:30pm','Tue'=>'8:30am–4:30pm','Wed'=>'8:30am–4:30pm','Thu'=>'8:30am–4:30pm','Fri'=>'8:30am–4:30pm','Sat'=>'Closed','Sun'=>'Closed']),39.4375,-78.9734,0],
|
||
['Mineral County Tourism Visitors Center','mineral-county-tourism',$GV,'Tourism / Visitors Center',
|
||
'167 S. Mineral Street — Your starting point for exploring Mineral County! Brochures, maps, and friendly staff.',
|
||
'167 S. Mineral St., Keyser, WV 26726','304-790-7081','mineralcocvb@gmail.com','govisitmineralwv.com',
|
||
$h(['Mon'=>'9am–4pm','Tue'=>'9am–4pm','Wed'=>'9am–4pm','Thu'=>'9am–4pm','Fri'=>'9am–4pm','Sat'=>'Call Ahead','Sun'=>'Closed']),39.4358,-78.9734,1],
|
||
['Mineral County Historical Society Museum','mineral-county-historical',$GV,'Museum / Historical Society',
|
||
"Museum tracking the history of Mineral County and Keyser with engaging exhibits and artifacts. Open by appointment.",
|
||
'Keyser, WV 26726','304-788-3066','','',
|
||
$h(['Mon'=>'By Appt','Tue'=>'By Appt','Wed'=>'By Appt','Thu'=>'By Appt','Fri'=>'By Appt','Sat'=>'Special Events','Sun'=>'Special Events']),39.4378,-78.9734,0],
|
||
// Churches
|
||
['Keyser First United Methodist Church','keyser-umc',$CH,'Methodist Church',
|
||
'Welcoming United Methodist congregation serving Keyser since the 19th century.',
|
||
'32 N. Davis St., Keyser, WV 26726','304-788-2522','','',
|
||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'Closed','Thu'=>'Closed','Fri'=>'Closed','Sat'=>'Closed','Sun'=>'Worship 11am']),39.4372,-78.9730,0],
|
||
['Living Faith Fellowship','living-faith',$CH,'Non-Denominational Church',
|
||
'Vibrant non-denominational Christian fellowship. Contemporary worship and community ministry.',
|
||
'1 N. Main St., Keyser, WV 26726','304-788-1910','','',
|
||
$h(['Mon'=>'Closed','Tue'=>'Closed','Wed'=>'7pm','Thu'=>'Closed','Fri'=>'Closed','Sat'=>'Closed','Sun'=>'Worship 10:30am']),39.4376,-78.9734,0],
|
||
];
|
||
|
||
foreach ($biz as $r) $bi->execute($r);
|
||
|
||
/* ── Menus ── */
|
||
$si = $db->prepare("INSERT INTO menu_sections(business_id,name,description,sort_order)VALUES(?,?,?,?)");
|
||
$mi = $db->prepare("INSERT INTO menu_items(section_id,name,description,price,sort_order)VALUES(?,?,?,?,?)");
|
||
|
||
$castId = (int)qval("SELECT id FROM businesses WHERE slug='castiglia-italian'");
|
||
foreach ([['Antipasti & Salads','Fresh starters',1],['Pasta','Handmade pastas',2],['Pizza','Stone-baked Italian pizzas',3],['Entrees','Secondi piatti',4],['Desserts','Dolci',5]] as $s) $si->execute(array_merge([$castId],$s));
|
||
$cs=[]; foreach(qall("SELECT id,name FROM menu_sections WHERE business_id=$castId") as $r) $cs[$r['name']]=$r['id'];
|
||
foreach(['Antipasti & Salads'=>[['Bruschetta al Pomodoro','Toasted rustic bread with vine-ripened tomatoes, fresh basil & extra-virgin olive oil',8.95,1],['Calamari Fritti','Lightly breaded fresh calamari, marinara sauce & lemon aioli',13.95,2],['Caprese Salad','Fresh buffalo mozzarella, heirloom tomatoes, basil & balsamic glaze',11.95,3],['Minestrone Soup','Classic Italian vegetable soup with cannellini beans',7.50,4],['Caesar Salad','Romaine, house Caesar dressing, shaved Parmigiano, croutons',9.95,5]],
|
||
'Pasta'=>[['Spaghetti Carbonara','Classic Roman — guanciale, egg, Pecorino Romano, cracked black pepper',15.95,1],['Fettuccine Alfredo','House-made fettuccine in rich cream & Parmigiano sauce',14.95,2],['Lasagna al Forno','House-rolled pasta, Bolognese, bechamel & Parmigiano',16.95,3],['Baked Ziti','Ziti, house marinara, ricotta & melted mozzarella',13.95,4],['Penne Arrabbiata','Spicy tomato, garlic, Calabrian chiles & fresh parsley',12.95,5]],
|
||
'Pizza'=>[['Margherita','San Marzano tomatoes, fior di latte mozzarella & fresh basil',14.95,1],['Pepperoni','House marinara, mozzarella & premium pepperoni',15.95,2],['Quattro Stagioni','Ham, mushrooms, artichoke hearts & Kalamata olives',17.95,3],['Prosciutto & Arugula','Parma prosciutto, baby arugula & shaved Parmigiano',18.95,4]],
|
||
'Entrees'=>[['Chicken Parmigiana','Breaded chicken, house marinara & fresh mozzarella, served with pasta',18.95,1],['Osso Buco alla Milanese','Braised veal shank, saffron risotto & traditional gremolata',34.95,2],['Eggplant Parmigiana','Layered eggplant, San Marzano sauce & mozzarella',15.95,3],['Salmon Piccata','Pan-seared Atlantic salmon, lemon-caper butter & fresh herbs',24.95,4]],
|
||
'Desserts'=>[['Tiramisu','Espresso-soaked ladyfingers, mascarpone cream & cocoa',8.95,1],['Cannoli Siciliani','Crispy shells with sweet ricotta, chocolate chips & pistachios',7.95,2],['Panna Cotta','Silky vanilla panna cotta with seasonal berry coulis',7.50,3],['Gelato del Giorno','Two scoops house-made gelato — ask your server',6.95,4]]]
|
||
as $sec=>$items) foreach($items as $item) $mi->execute(array_merge([$cs[$sec]],$item));
|
||
|
||
// Queens Point Coffee menu
|
||
$qpId = (int)qval("SELECT id FROM businesses WHERE slug='queens-point-coffee'");
|
||
foreach ([['Espresso Drinks','Hot & cold espresso beverages',1],['Cold Drinks','Refreshing chilled options',2],['Food','Fresh baked goods & light bites',3]] as $s) $si->execute(array_merge([$qpId],$s));
|
||
$qs=[]; foreach(qall("SELECT id,name FROM menu_sections WHERE business_id=$qpId") as $r) $qs[$r['name']]=$r['id'];
|
||
foreach(['Espresso Drinks'=>[['Latte','Smooth espresso with steamed milk',4.50,1],['Cappuccino','Double espresso with foamed milk',4.25,2],['Americano','Espresso shots with hot water',3.50,3],['Mocha','Espresso, chocolate & steamed milk',5.00,4],['Iced Matcha Latte','Matcha with milk — our most raved drink!',5.25,5]],
|
||
'Cold Drinks'=>[['Cold Brew','Smooth 24-hour cold brew over ice',3.75,1],['Iced Latte','Chilled espresso with cold milk',4.75,2],['Blended Frappe','Blended ice coffee drink',5.50,3]],
|
||
'Food'=>[['Blueberry Muffin','Fresh-baked daily',3.00,1],['Avocado Toast','Multigrain with avocado & everything spice',7.50,2],['Breakfast Sandwich','Egg, cheese & choice of meat',6.50,3],['Cookie','Assorted fresh-baked cookies',2.50,4]]]
|
||
as $sec=>$items) foreach($items as $item) $mi->execute(array_merge([$qs[$sec]],$item));
|
||
|
||
/* ── Alerts ── */
|
||
foreach ([
|
||
['castiglia-italian','news','New Menu Items This Season!',
|
||
"Chef's new handmade gnocchi and wood-fired seasonal pizza are now available. Fresh locally-sourced summer ingredients throughout the menu."],
|
||
['queens-point-coffee','news','Try Our Famous Iced Matcha!',
|
||
'"THE best coffee shop I\'ve ever been to." Our iced matcha with coconut and pistachio is the most-talked-about drink in Keyser.'],
|
||
['clancys-irish-pub','event','Live Music Every Friday & Saturday!',
|
||
"Live music from 9pm-1am every Friday and Saturday night. No cover charge! Food and drinks served until close."],
|
||
['pvh','info','Urgent Care Now Open 7 Days',
|
||
'WVU Medicine Potomac Valley Hospital Urgent Care is open Mon-Fri 8am-8pm and weekends 9am-5pm. No appointment needed.'],
|
||
['north-branch-ventures','event','Guided Kayak Tours - Book Now!',
|
||
"Guided half-day kayak tours on the North Branch Potomac every Saturday and Sunday through October. All skill levels welcome."],
|
||
] as [$slug,$type,$title,$body])
|
||
qrun("INSERT INTO alerts(business_id,type,title,body,is_active)VALUES((SELECT id FROM businesses WHERE slug=?),?,?,?,1)",[$slug,$type,$title,$body]);
|
||
|
||
/* ── Attractions ── */
|
||
foreach ([
|
||
['Queens Point Overlook','Natural Landmark',"A spectacular Oriskany sandstone cliff rising ~400 feet above the North Branch Potomac River. Breathtaking panoramic views of Keyser and the river valley.",'McCoole, MD (viewable from Keyser riverfront)','','','Accessible year-round',1,1],
|
||
['North Branch Potomac River','Outdoor Recreation',"World-class smallmouth bass fishing, kayaking, canoeing, and bird watching. Contact North Branch Ventures for rentals and guided tours.",'Armstrong St. riverfront, Keyser, WV','304-790-7125','','Open year-round',1,2],
|
||
['Jennings Randolph Lake','Lake / Reservoir',"A stunning 952-acre reservoir on the North Branch Potomac. Boating, fishing, swimming, camping, and picnicking. Managed by U.S. Army Corps of Engineers.",'Elk Garden, WV 26717 (~20 min from Keyser)','304-355-2346','','Open year-round',1,3],
|
||
['Larenim Park','County Park',"Mineral County's premier park at 365 acres. Pavilions, 600-seat amphitheater, fishing ponds, and 5 miles of trails.",'Keyser, WV 26726','304-788-3066','','Open daily',1,4],
|
||
['Mineral County Courthouse','Historic Site',"The 1868 courthouse listed on the National Register of Historic Places — the architectural centerpiece of downtown Keyser.",'Keyser, WV 26726','304-788-3003','','Exterior always viewable',1,5],
|
||
['Potomac State College Campus / Fort Fuller','Historic Site',"Fort Hill — site of Civil War Fort Fuller — commanded by future president Benjamin Harrison and Lew Wallace (author of Ben-Hur). Now the beautiful PSC campus.",'101 Fort Ave., Keyser, WV 26726','304-788-6800','potomacstatecollege.edu','Campus open daily',1,6],
|
||
['Potomac Eagle Scenic Railroad','Train Excursion',"Scenic excursions through The Trough along the South Branch Potomac River — famous for bald eagle sightings. About 30 miles from Keyser.",'Romney, WV 26757 (~30 miles)','304-822-7400','potomaceagle.info','Seasonal spring through fall',1,7],
|
||
['Blackwater Falls State Park','State Park',"The amber-colored Blackwater Falls plunge 57 feet into the dramatic canyon. Hiking, skiing, camping, and a beautiful lodge. About 40 minutes from Keyser.",'Davis, WV 26260 (~40 min)','304-259-5216','wvstateparks.com','Open year-round',1,8],
|
||
['Seneca Rocks','Natural Landmark',"Massive quartzite outcroppings rising 900 feet from the valley floor. Rock climbing, hiking, and spectacular views. About 45 minutes from Keyser.",'Seneca Rocks, WV (~45 min)','304-567-2827','','Open year-round',1,9],
|
||
['Fort Ashby Blockhouse','Historic Site',"The only remaining French and Indian War fort in West Virginia. Ordered built by young George Washington in 1755. Just 15 miles from Keyser.",'Fort Ashby, WV 26719 (~15 miles)','','','Open seasonally',1,10],
|
||
] as $a) qrun("INSERT INTO attractions(name,category,description,address,phone,website,hours,is_active,sort_order)VALUES(?,?,?,?,?,?,?,?,?)",$a);
|
||
|
||
/* ── Events ── */
|
||
$adminId=(int)qval("SELECT id FROM users WHERE username='administrator'");
|
||
foreach ([
|
||
['Mineral County Fair','Annual county fair featuring livestock shows, carnival rides, food vendors, live entertainment, and the best of Mineral County agriculture.','Mineral County Fairgrounds','Fort Ashby, WV','2025-08-12','9:00 AM','2025-08-16','10:00 PM','Admission varies','mineralcountyfair.org','','','304-788-0000',$adminId,'approved',1],
|
||
['Keyser Christmas Parade & Tree Lighting','Annual holiday parade through downtown Keyser with floats, marching bands, Santa Claus, and tree lighting.','Downtown Main Street','Keyser, WV 26726','2025-12-06','5:00 PM','2025-12-06','7:00 PM','Free','cityofkeyser.com','','','304-788-0222',$adminId,'approved',1],
|
||
['PSC Catamount Athletics','Support the WVU Potomac State Catamounts at home athletic events throughout the semester.','Potomac State College','101 Fort Ave., Keyser','2025-09-01','Varies','2025-11-30','Varies','Free–$5','potomacstatecollege.edu','','','304-788-6800',$adminId,'approved',0],
|
||
['North Branch Kayak Adventure Series','Guided and recreational kayaking on the North Branch Potomac every Saturday through October.','North Branch Ventures','101 Armstrong St., Keyser','2025-05-01','8:00 AM','2025-10-31','4:00 PM','$25–$45','','','','304-790-7125',$adminId,'approved',0],
|
||
] as $ev) qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,submitted_by,status,is_featured)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",$ev);
|
||
}
|