- [index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/index.php): events now appear before featured businesses, plus forum pretty-route fallback.
[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.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
data/*.db
|
||||
data/*.db-shm
|
||||
data/*.db-wal
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
RewriteEngine On
|
||||
RewriteRule ^forum/?$ forum.php [L,QSA]
|
||||
RewriteRule ^forum/([^/]+)/?$ forum.php?topic=$1 [L,QSA]
|
||||
+95
-3
@@ -7,6 +7,7 @@ $errors = [];
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$em = ps('email'); $pw = ps('password'); $pw2 = ps('password2');
|
||||
$avatarPath = (string)($u['avatar_path'] ?? '');
|
||||
if ($em && !filter_var($em,FILTER_VALIDATE_EMAIL)) $errors[] = 'Invalid email address.';
|
||||
if ($pw) {
|
||||
if (strlen($pw) < 6) $errors[] = 'Password must be at least 6 characters.';
|
||||
@@ -14,11 +15,18 @@ if (isPost()) {
|
||||
}
|
||||
if (!$errors && $em && $em !== $u['email'] && qval("SELECT id FROM users WHERE email=? AND id!=?",[$em,uid()])) $errors[] = 'Email already in use.';
|
||||
if (!$errors) {
|
||||
if ($pw) qrun("UPDATE users SET email=?,password=? WHERE id=?",[$em?:null,password_hash($pw,PASSWORD_BCRYPT),uid()]);
|
||||
else qrun("UPDATE users SET email=? WHERE id=?",[$em?:null,uid()]);
|
||||
if (p('remove_avatar','0') === '1') $avatarPath = '';
|
||||
$avatar = storeAvatarUpload('avatar', mbToBytes(setting('avatar_upload_max_mb','3'), 3));
|
||||
if (!$avatar['ok']) $errors[] = $avatar['error'];
|
||||
elseif (!empty($avatar['path'])) $avatarPath = (string)$avatar['path'];
|
||||
}
|
||||
if (!$errors) {
|
||||
if ($pw) qrun("UPDATE users SET email=?,avatar_path=?,password=? WHERE id=?",[$em?:null,$avatarPath,password_hash($pw,PASSWORD_BCRYPT),uid()]);
|
||||
else qrun("UPDATE users SET email=?,avatar_path=? WHERE id=?",[$em?:null,$avatarPath,uid()]);
|
||||
flash('Account updated successfully!','success'); go('/account.php');
|
||||
}
|
||||
$u['email'] = $em;
|
||||
$u['avatar_path'] = $avatarPath;
|
||||
}
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
@@ -27,13 +35,35 @@ include __DIR__.'/includes/header.php';
|
||||
<p class="form-sub">Manage your Discover Keyser WV account</p>
|
||||
<div class="ibox" style="margin-bottom:1.5rem">
|
||||
<div class="ibox-title">ACCOUNT INFO</div>
|
||||
<div class="account-avatar-preview"><?=userAvatarHtml($u, 'avatar-lg')?></div>
|
||||
<div class="irow"><span class="irow-i">👤</span><span class="irow-v"><?=e($u['username'])?> <?=isAdmin()?'<span class="badge badge-gold">ADMIN</span>':''?></span></div>
|
||||
<div class="irow"><span class="irow-i">📅</span><span class="irow-v">Joined <?=fdate($u['created_at'])?></span></div>
|
||||
<?php if($u['last_login']): ?><div class="irow"><span class="irow-i">🕐</span><span class="irow-v">Last login: <?=ago($u['last_login'])?></span></div><?php endif; ?>
|
||||
</div>
|
||||
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
|
||||
<form method="POST">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<?=csrfField()?>
|
||||
<div class="fg">
|
||||
<label class="flabel">PROFILE AVATAR</label>
|
||||
<div class="avatar-upload-grid">
|
||||
<canvas id="avatarCanvas" class="avatar-canvas" width="240" height="240" aria-label="Avatar crop preview"></canvas>
|
||||
<div>
|
||||
<input type="file" name="avatar" id="avatarInput" class="finput" accept="image/*">
|
||||
<div class="avatar-crop-controls" id="avatarCropControls">
|
||||
<label>Zoom <input type="range" id="avatarZoom" min="1" max="3" step="0.01" value="1"></label>
|
||||
<label>Horizontal <input type="range" id="avatarFocusX" min="0" max="100" value="50"></label>
|
||||
<label>Vertical <input type="range" id="avatarFocusY" min="0" max="100" value="50"></label>
|
||||
</div>
|
||||
<input type="hidden" name="avatar_crop_x" id="avatarCropX" value="0">
|
||||
<input type="hidden" name="avatar_crop_y" id="avatarCropY" value="0">
|
||||
<input type="hidden" name="avatar_crop_size" id="avatarCropSize" value="0">
|
||||
<div class="fhint">Optional JPG, PNG, GIF, or WebP. Max <?=e(setting('avatar_upload_max_mb','3'))?> MB.</div>
|
||||
<?php if(!empty($u['avatar_path'])): ?>
|
||||
<label style="display:flex;align-items:center;gap:.45rem;margin-top:.6rem;font-size:.85rem;color:var(--text-m);cursor:pointer"><input type="checkbox" name="remove_avatar" value="1"> Remove current avatar</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">EMAIL ADDRESS</label><input type="email" name="email" class="finput" value="<?=e($u['email']??'')?>"></div>
|
||||
<hr class="divider" style="margin:1.25rem 0">
|
||||
<p style="font-family:'Oswald',sans-serif;font-size:.72rem;letter-spacing:.1em;color:var(--gold);margin-bottom:.85rem">CHANGE PASSWORD (leave blank to keep current)</p>
|
||||
@@ -42,4 +72,66 @@ include __DIR__.'/includes/header.php';
|
||||
<button type="submit" class="btn btn-primary btn-block">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php $footExtra = <<<'HTML'
|
||||
<script>
|
||||
(function(){
|
||||
const input = document.getElementById('avatarInput');
|
||||
const canvas = document.getElementById('avatarCanvas');
|
||||
if (!input || !canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const zoom = document.getElementById('avatarZoom');
|
||||
const fx = document.getElementById('avatarFocusX');
|
||||
const fy = document.getElementById('avatarFocusY');
|
||||
const cx = document.getElementById('avatarCropX');
|
||||
const cy = document.getElementById('avatarCropY');
|
||||
const cs = document.getElementById('avatarCropSize');
|
||||
let img = null;
|
||||
|
||||
function drawEmpty(){
|
||||
ctx.clearRect(0,0,240,240);
|
||||
ctx.fillStyle = '#121f38';
|
||||
ctx.fillRect(0,0,240,240);
|
||||
ctx.strokeStyle = 'rgba(38,244,255,.35)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(120,120,104,0,Math.PI*2);
|
||||
ctx.stroke();
|
||||
}
|
||||
function redraw(){
|
||||
if (!img) { drawEmpty(); return; }
|
||||
const z = parseFloat(zoom.value || '1');
|
||||
const natural = Math.min(img.naturalWidth, img.naturalHeight);
|
||||
const size = Math.max(1, Math.round(natural / z));
|
||||
const x = Math.round((img.naturalWidth - size) * (parseInt(fx.value,10) / 100));
|
||||
const y = Math.round((img.naturalHeight - size) * (parseInt(fy.value,10) / 100));
|
||||
cx.value = x; cy.value = y; cs.value = size;
|
||||
ctx.clearRect(0,0,240,240);
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(120,120,118,0,Math.PI*2);
|
||||
ctx.clip();
|
||||
ctx.drawImage(img, x, y, size, size, 0, 0, 240, 240);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(255,209,102,.9)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(120,120,118,0,Math.PI*2);
|
||||
ctx.stroke();
|
||||
}
|
||||
input.addEventListener('change', function(){
|
||||
const file = input.files && input.files[0];
|
||||
if (!file) { img = null; redraw(); return; }
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(){
|
||||
img = new Image();
|
||||
img.onload = redraw;
|
||||
img.src = reader.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
[zoom, fx, fy].forEach(el => el && el.addEventListener('input', redraw));
|
||||
drawEmpty();
|
||||
})();
|
||||
</script>
|
||||
HTML; ?>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
|
||||
@@ -25,6 +25,7 @@ $_pendingClaims = (int)qval("SELECT COUNT(*) FROM business_claim_requests WHERE
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<div class="admin-side-title" style="margin-top:1rem">COMMUNITY</div>
|
||||
<a class="asl" href="/admin/forum.php">💬 Forum</a>
|
||||
<a class="asl" href="/admin/events.php">📅 Events</a>
|
||||
<a class="asl" href="/admin/attractions.php">🗺️ Attractions</a>
|
||||
<a class="asl" href="/admin/reports.php">🚩 Reports</a>
|
||||
|
||||
+40
-7
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
requireAdmin();
|
||||
$adminTitle = 'Events';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
@@ -12,18 +13,36 @@ if (isPost()) {
|
||||
if ($act === 'feature') { qrun("UPDATE events SET is_featured=1-is_featured WHERE id=?",[(int)p('id')]); go('/admin/events.php'); }
|
||||
|
||||
if ($act === 'add' || $act === 'edit') {
|
||||
$id = (int)p('id');
|
||||
$existing = $act === 'edit' ? qone("SELECT image_path FROM events WHERE id=?", [$id]) : null;
|
||||
$imagePath = (string)($existing['image_path'] ?? '');
|
||||
if (p('remove_image','0') === '1') $imagePath = '';
|
||||
$upload = storeSingleUpload(
|
||||
'event_image',
|
||||
'events',
|
||||
imageExts(),
|
||||
mbToBytes(setting('event_upload_max_mb','5'), 5),
|
||||
[
|
||||
'image_only' => true,
|
||||
'resize_over_bytes' => mbToBytes(setting('forum_image_resize_threshold_mb','1'), 1),
|
||||
'image_max_width' => (int)setting('forum_image_max_width','1600'),
|
||||
'image_quality' => (int)setting('forum_image_quality','82'),
|
||||
]
|
||||
);
|
||||
if (!$upload['ok']) { flash($upload['error'], 'error'); go('/admin/events.php'.($id ? '?edit='.$id : '')); }
|
||||
if (!empty($upload['path'])) $imagePath = (string)$upload['path'];
|
||||
|
||||
$f=['title'=>ps('title'),'description'=>ps('description'),'venue'=>ps('venue'),'address'=>ps('address'),
|
||||
'event_date'=>ps('event_date'),'event_time'=>ps('event_time'),'end_date'=>ps('end_date')?:null,
|
||||
'end_time'=>ps('end_time'),'cost'=>ps('cost'),'website'=>ps('website'),'contact_name'=>ps('contact_name'),
|
||||
'contact_email'=>ps('contact_email'),'contact_phone'=>ps('contact_phone'),
|
||||
'contact_email'=>ps('contact_email'),'contact_phone'=>ps('contact_phone'),'image_path'=>$imagePath,
|
||||
'status'=>p('status','approved'),'is_featured'=>(int)p('is_featured',0)];
|
||||
if (!$f['title'] || !$f['event_date']) { flash('Title and date required.','error'); go('/admin/events.php'); }
|
||||
if ($act === 'add') {
|
||||
qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,status,is_featured,submitted_by)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",array_merge(array_values($f),[uid()]));
|
||||
qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,image_path,status,is_featured,submitted_by)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",array_merge(array_values($f),[uid()]));
|
||||
flash('Event added!','success');
|
||||
} else {
|
||||
$id = (int)p('id');
|
||||
qrun("UPDATE events SET title=?,description=?,venue=?,address=?,event_date=?,event_time=?,end_date=?,end_time=?,cost=?,website=?,contact_name=?,contact_email=?,contact_phone=?,status=?,is_featured=? WHERE id=?",array_merge(array_values($f),[$id]));
|
||||
qrun("UPDATE events SET title=?,description=?,venue=?,address=?,event_date=?,event_time=?,end_date=?,end_time=?,cost=?,website=?,contact_name=?,contact_email=?,contact_phone=?,image_path=?,status=?,is_featured=? WHERE id=?",array_merge(array_values($f),[$id]));
|
||||
flash('Event updated!','success');
|
||||
}
|
||||
go('/admin/events.php');
|
||||
@@ -35,12 +54,13 @@ $editId = iget('edit');
|
||||
$editing = $editId ? qone("SELECT * FROM events WHERE id=?",[$editId]) : null;
|
||||
$where = match($filter) { 'pending'=>"WHERE status='pending'",'approved'=>"WHERE status='approved'", default=>"" };
|
||||
$events = qall("SELECT e.*,u.username FROM events e LEFT JOIN users u ON u.id=e.submitted_by $where ORDER BY CASE status WHEN 'pending' THEN 0 ELSE 1 END,event_date DESC");
|
||||
include __DIR__.'/admin_header.php';
|
||||
?>
|
||||
<?php adminTitle('Manage Events') ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d);margin-bottom:2rem">
|
||||
<div class="ibox-title"><?=$editing ? '✏️ EDIT EVENT: '.strtoupper(e($editing['title'])) : '➕ ADD EVENT'?></div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="<?=$editing?'edit':'add'?>">
|
||||
<form method="POST" enctype="multipart/form-data"><?=csrfField()?><input type="hidden" name="_act" value="<?=$editing?'edit':'add'?>">
|
||||
<?php if($editing): ?><input type="hidden" name="id" value="<?=$editing['id']?>"><?php endif; ?>
|
||||
<div class="fg"><label class="flabel">TITLE *</label><input type="text" name="title" class="finput" value="<?=e($editing['title']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><textarea name="description" class="ftextarea" style="min-height:80px"><?=e($editing['description']??'')?></textarea></div>
|
||||
@@ -60,6 +80,19 @@ $events = qall("SELECT e.*,u.username FROM events e LEFT JOIN users u ON u.id=e.
|
||||
<div class="fg"><label class="flabel">COST</label><input type="text" name="cost" class="finput" value="<?=e($editing['cost']??'Free')?>"></div>
|
||||
<div class="fg"><label class="flabel">WEBSITE</label><input type="text" name="website" class="finput" value="<?=e($editing['website']??'')?>"></div>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">EVENT IMAGE</label>
|
||||
<?php if(!empty($editing['image_path'])): ?>
|
||||
<div class="event-admin-image">
|
||||
<img src="<?=e($editing['image_path'])?>" alt="<?=e($editing['title'] ?? 'Event image')?>">
|
||||
<label style="display:flex;align-items:center;gap:.45rem;font-size:.85rem;color:var(--text-m);cursor:pointer">
|
||||
<input type="checkbox" name="remove_image" value="1"> Remove current image
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="event_image" class="finput" accept="image/*">
|
||||
<div class="fhint">Optional JPG, PNG, GIF, or WebP. Max <?=e(setting('event_upload_max_mb','5'))?> MB.</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">CONTACT NAME</label><input type="text" name="contact_name" class="finput" value="<?=e($editing['contact_name']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">CONTACT PHONE</label><input type="text" name="contact_phone" class="finput" value="<?=e($editing['contact_phone']??'')?>"></div>
|
||||
@@ -90,7 +123,7 @@ $events = qall("SELECT e.*,u.username FROM events e LEFT JOIN users u ON u.id=e.
|
||||
<tbody>
|
||||
<?php foreach($events as $ev): ?>
|
||||
<tr>
|
||||
<td class="td-name" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?=e($ev['title'])?></td>
|
||||
<td class="td-name" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?=e($ev['title'])?><?=!empty($ev['image_path'])?' <span class="badge badge-blue">Image</span>':''?></td>
|
||||
<td style="font-size:.8rem"><?=fdate($ev['event_date'])?></td>
|
||||
<td style="font-size:.8rem"><?=e($ev['username']??'Admin')?></td>
|
||||
<td><?php $sc=['approved'=>'badge-green','pending'=>'badge-gold','rejected'=>'badge-red']; ?><span class="badge <?=$sc[$ev['status']]??"badge-gray"?>"><?=strtoupper($ev['status'])?></span></td>
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
requireAdmin();
|
||||
|
||||
function forumCategorySlugForAdmin(string $name, int $id = 0): string {
|
||||
$base = makeSlug($name) ?: 'category';
|
||||
$slug = $base;
|
||||
$i = 1;
|
||||
while (qval("SELECT id FROM forum_categories WHERE slug=? AND id!=?", [$slug, $id])) {
|
||||
$slug = $base.'-'.$i++;
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
$act = p('_act');
|
||||
|
||||
if ($act === 'settings') {
|
||||
setSetting('forum_enabled', p('forum_enabled','0') === '1' ? '1' : '0');
|
||||
setSetting('forum_upload_max_mb', (string)max(1, (float)p('forum_upload_max_mb','5')));
|
||||
$allowed = strtolower(ps('forum_allowed_filetypes'));
|
||||
$allowed = implode(',', uploadAllowedExts($allowed, ['jpg','jpeg','png','gif','webp','pdf','txt','doc','docx']));
|
||||
setSetting('forum_allowed_filetypes', $allowed);
|
||||
setSetting('forum_image_resize_threshold_mb', (string)max(0.5, (float)p('forum_image_resize_threshold_mb','1')));
|
||||
setSetting('forum_image_max_width', (string)max(640, min(3200, (int)p('forum_image_max_width','1600'))));
|
||||
setSetting('forum_image_quality', (string)max(40, min(95, (int)p('forum_image_quality','82'))));
|
||||
flash('Forum settings saved.', 'success');
|
||||
go('/admin/forum.php');
|
||||
}
|
||||
|
||||
if ($act === 'add_category') {
|
||||
$name = ps('name');
|
||||
if ($name !== '') {
|
||||
qrun("INSERT INTO forum_categories(name,slug,description,sort_order,is_active)VALUES(?,?,?,?,?)",
|
||||
[$name, forumCategorySlugForAdmin($name), ps('description'), (int)p('sort_order',0), (int)p('is_active',1)]);
|
||||
flash('Category added.', 'success');
|
||||
}
|
||||
go('/admin/forum.php#categories');
|
||||
}
|
||||
|
||||
if ($act === 'edit_category') {
|
||||
$id = (int)p('id');
|
||||
$name = ps('name');
|
||||
if ($id && $name !== '') {
|
||||
qrun("UPDATE forum_categories SET name=?,slug=?,description=?,sort_order=?,is_active=? WHERE id=?",
|
||||
[$name, forumCategorySlugForAdmin($name, $id), ps('description'), (int)p('sort_order',0), (int)p('is_active',1), $id]);
|
||||
flash('Category updated.', 'success');
|
||||
}
|
||||
go('/admin/forum.php#categories');
|
||||
}
|
||||
|
||||
if ($act === 'delete_category') {
|
||||
$id = (int)p('id');
|
||||
$topicCount = (int)qval("SELECT COUNT(*) FROM forum_topics WHERE category_id=?", [$id]);
|
||||
if ($topicCount > 0) {
|
||||
qrun("UPDATE forum_categories SET is_active=0 WHERE id=?", [$id]);
|
||||
flash('Category has topics, so it was deactivated instead of deleted.', 'warning');
|
||||
} else {
|
||||
qrun("DELETE FROM forum_categories WHERE id=?", [$id]);
|
||||
flash('Category deleted.', 'success');
|
||||
}
|
||||
go('/admin/forum.php#categories');
|
||||
}
|
||||
|
||||
if (in_array($act, ['toggle_pin','toggle_lock','delete_topic'], true)) {
|
||||
$id = (int)p('id');
|
||||
if ($act === 'toggle_pin') {
|
||||
qrun("UPDATE forum_topics SET is_pinned=1-is_pinned WHERE id=?", [$id]);
|
||||
flash('Topic pin status updated.', 'success');
|
||||
} elseif ($act === 'toggle_lock') {
|
||||
qrun("UPDATE forum_topics SET is_locked=1-is_locked WHERE id=?", [$id]);
|
||||
flash('Topic lock status updated.', 'success');
|
||||
} else {
|
||||
qrun("DELETE FROM forum_topics WHERE id=?", [$id]);
|
||||
flash('Topic deleted.', 'success');
|
||||
}
|
||||
go('/admin/forum.php#topics');
|
||||
}
|
||||
}
|
||||
|
||||
$adminTitle = 'Forum';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
$forumEnabled = setting('forum_enabled','1');
|
||||
$forumUploadMax = setting('forum_upload_max_mb','5');
|
||||
$forumAllowed = setting('forum_allowed_filetypes','jpg,jpeg,png,gif,webp,pdf,txt,doc,docx');
|
||||
$resizeThreshold = setting('forum_image_resize_threshold_mb','1');
|
||||
$resizeMaxWidth = setting('forum_image_max_width','1600');
|
||||
$resizeQuality = setting('forum_image_quality','82');
|
||||
$counts = [
|
||||
'Topics' => qval("SELECT COUNT(*) FROM forum_topics"),
|
||||
'Replies' => qval("SELECT COUNT(*) FROM forum_replies"),
|
||||
'Categories' => qval("SELECT COUNT(*) FROM forum_categories"),
|
||||
'Attachments' => qval("SELECT COUNT(*) FROM forum_attachments"),
|
||||
];
|
||||
$categories = qall("
|
||||
SELECT fc.*, COUNT(ft.id) topic_count
|
||||
FROM forum_categories fc
|
||||
LEFT JOIN forum_topics ft ON ft.category_id=fc.id
|
||||
GROUP BY fc.id
|
||||
ORDER BY fc.sort_order, fc.name
|
||||
");
|
||||
$topics = qall("
|
||||
SELECT ft.*, fc.name category_name, u.username,
|
||||
(SELECT COUNT(*) FROM forum_replies fr WHERE fr.topic_id=ft.id) reply_count
|
||||
FROM forum_topics ft
|
||||
JOIN forum_categories fc ON fc.id=ft.category_id
|
||||
JOIN users u ON u.id=ft.user_id
|
||||
ORDER BY ft.is_pinned DESC, datetime(ft.updated_at) DESC
|
||||
LIMIT 60
|
||||
");
|
||||
?>
|
||||
<?php adminTitle('Forum Controls', 'Enable the forum, manage upload rules, categories, and recent topics.') ?>
|
||||
|
||||
<div class="stat-cards">
|
||||
<?php foreach($counts as $label=>$value): ?>
|
||||
<div class="stat-card"><div class="sc-n"><?=$value?></div><div class="sc-l"><?=e($label)?></div></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="g2" style="gap:1.5rem">
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">FORUM SETTINGS</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="settings">
|
||||
<div class="fg">
|
||||
<label class="flabel">FORUM VISIBILITY</label>
|
||||
<div style="display:flex;gap:1rem;flex-wrap:wrap">
|
||||
<label style="display:flex;gap:.45rem;align-items:center;color:var(--green-l);cursor:pointer"><input type="radio" name="forum_enabled" value="1"<?=$forumEnabled==='1'?' checked':''?>> Enabled for members</label>
|
||||
<label style="display:flex;gap:.45rem;align-items:center;color:var(--red-l);cursor:pointer"><input type="radio" name="forum_enabled" value="0"<?=$forumEnabled==='0'?' checked':''?>> Disabled for non-admins</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">FILE MAX MB</label><input type="number" name="forum_upload_max_mb" class="finput" min="1" step="0.5" value="<?=e($forumUploadMax)?>"></div>
|
||||
<div class="fg"><label class="flabel">ALLOWED FILE TYPES</label><input type="text" name="forum_allowed_filetypes" class="finput" value="<?=e($forumAllowed)?>"><div class="fhint">Comma-separated extensions.</div></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">RESIZE IMAGES OVER MB</label><input type="number" name="forum_image_resize_threshold_mb" class="finput" min="0.5" step="0.5" value="<?=e($resizeThreshold)?>"></div>
|
||||
<div class="fg"><label class="flabel">MAX IMAGE WIDTH</label><input type="number" name="forum_image_max_width" class="finput" min="640" max="3200" step="80" value="<?=e($resizeMaxWidth)?>"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">IMAGE QUALITY</label><input type="number" name="forum_image_quality" class="finput" min="40" max="95" value="<?=e($resizeQuality)?>"></div>
|
||||
<button class="btn btn-primary">Save Forum Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="ibox" id="categories" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">ADD CATEGORY</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add_category">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">NAME</label><input type="text" name="name" class="finput" required></div>
|
||||
<div class="fg"><label class="flabel">SORT ORDER</label><input type="number" name="sort_order" class="finput" value="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="description" class="finput"></div>
|
||||
<div class="fg"><label class="flabel">STATUS</label><select name="is_active" class="fselect"><option value="1">Active</option><option value="0">Inactive</option></select></div>
|
||||
<button class="btn btn-primary btn-sm">Add Category</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-sec-title" style="margin-top:1.5rem">CATEGORIES</div>
|
||||
<div class="tbl-wrap" style="margin-bottom:2rem">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>Name</th><th>Description</th><th>Topics</th><th>Status</th><th>Order</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($cat['name'])?></td>
|
||||
<td style="font-size:.8rem"><?=e($cat['description'])?></td>
|
||||
<td><?=$cat['topic_count']?></td>
|
||||
<td><?=$cat['is_active']?'<span class="badge badge-green">Active</span>':'<span class="badge badge-gray">Inactive</span>'?></td>
|
||||
<td><?=$cat['sort_order']?></td>
|
||||
<td>
|
||||
<details>
|
||||
<summary class="btn btn-xs btn-outline" style="display:inline-flex">Edit</summary>
|
||||
<form method="POST" class="admin-inline-editor">
|
||||
<?=csrfField()?><input type="hidden" name="_act" value="edit_category"><input type="hidden" name="id" value="<?=$cat['id']?>">
|
||||
<div class="form-row">
|
||||
<input type="text" name="name" class="finput" value="<?=e($cat['name'])?>" required>
|
||||
<input type="number" name="sort_order" class="finput" value="<?=$cat['sort_order']?>">
|
||||
</div>
|
||||
<input type="text" name="description" class="finput" value="<?=e($cat['description'])?>">
|
||||
<select name="is_active" class="fselect"><option value="1"<?=$cat['is_active']?' selected':''?>>Active</option><option value="0"<?=!$cat['is_active']?' selected':''?>>Inactive</option></select>
|
||||
<button class="btn btn-success btn-xs">Save</button>
|
||||
</form>
|
||||
</details>
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="delete_category"><input type="hidden" name="id" value="<?=$cat['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete this category?">Delete</button></form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-sec-title" id="topics">RECENT TOPICS</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>Topic</th><th>Category</th><th>By</th><th>Replies</th><th>Status</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($topics as $topic): ?>
|
||||
<tr>
|
||||
<td><a class="td-name" href="<?=e(forumTopicUrl($topic))?>" target="_blank"><?=e($topic['title'])?></a><div style="font-size:.75rem;color:var(--text-d)"><?=ago($topic['updated_at'])?></div></td>
|
||||
<td><?=e($topic['category_name'])?></td>
|
||||
<td><?=e($topic['username'])?></td>
|
||||
<td><?=$topic['reply_count']?></td>
|
||||
<td>
|
||||
<?php if($topic['is_pinned']): ?><span class="badge badge-gold">Pinned</span><?php endif; ?>
|
||||
<?php if($topic['is_locked']): ?><span class="badge badge-gray">Locked</span><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex-row" style="gap:.3rem">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="toggle_pin"><input type="hidden" name="id" value="<?=$topic['id']?>"><button class="btn btn-xs btn-outline"><?=$topic['is_pinned']?'Unpin':'Pin'?></button></form>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="toggle_lock"><input type="hidden" name="id" value="<?=$topic['id']?>"><button class="btn btn-xs btn-secondary"><?=$topic['is_locked']?'Unlock':'Lock'?></button></form>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="delete_topic"><input type="hidden" name="id" value="<?=$topic['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete this topic and replies?">Delete</button></form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if(!$topics): ?><tr><td colspan="6" style="text-align:center;color:var(--text-d)">No forum topics yet.</td></tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__.'/admin_footer.php'; ?>
|
||||
@@ -9,6 +9,7 @@ $stats = [
|
||||
'Pending Events' => qval("SELECT COUNT(*) FROM events WHERE status='pending'"),
|
||||
'Pending Claims' => qval("SELECT COUNT(*) FROM business_claim_requests WHERE status='pending'"),
|
||||
'Pending Edits' => qval("SELECT COUNT(*) FROM business_edit_requests WHERE status='pending'"),
|
||||
'Forum Topics' => qval("SELECT COUNT(*) FROM forum_topics"),
|
||||
'Open Reports' => qval("SELECT COUNT(*) FROM reports WHERE status='open'"),
|
||||
'Active Alerts' => qval("SELECT COUNT(*) FROM alerts WHERE is_active=1"),
|
||||
];
|
||||
@@ -34,6 +35,15 @@ $pendingEdits = qall("
|
||||
ORDER BY ber.created_at DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$recentForumTopics = qall("
|
||||
SELECT ft.*, fc.name category_name, u.username,
|
||||
(SELECT COUNT(*) FROM forum_replies fr WHERE fr.topic_id=ft.id) reply_count
|
||||
FROM forum_topics ft
|
||||
JOIN forum_categories fc ON fc.id=ft.category_id
|
||||
JOIN users u ON u.id=ft.user_id
|
||||
ORDER BY datetime(ft.updated_at) DESC
|
||||
LIMIT 5
|
||||
");
|
||||
?>
|
||||
<?php adminTitle('Site Overview','Welcome to the Discover Keyser WV administration panel.') ?>
|
||||
|
||||
@@ -156,6 +166,28 @@ $pendingEdits = qall("
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if($recentForumTopics): ?>
|
||||
<div style="margin-bottom:2rem">
|
||||
<div class="admin-sec-title">FORUM ACTIVITY</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>TOPIC</th><th>CATEGORY</th><th>BY</th><th>REPLIES</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($recentForumTopics as $topic): ?>
|
||||
<tr>
|
||||
<td><a href="<?=e(forumTopicUrl($topic))?>" target="_blank" class="td-name"><?=e($topic['title'])?></a><div style="font-size:.75rem;color:var(--text-d)"><?=ago($topic['updated_at'])?></div></td>
|
||||
<td><?=e($topic['category_name'])?></td>
|
||||
<td><?=e($topic['username'])?></td>
|
||||
<td><?=$topic['reply_count']?></td>
|
||||
<td><a href="/admin/forum.php#topics" class="btn btn-xs btn-primary">Moderate</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="g2">
|
||||
<div>
|
||||
<div class="admin-sec-title">🏢 RECENT BUSINESSES</div>
|
||||
|
||||
+62
-6
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
requireAdmin();
|
||||
$adminTitle = 'Menus';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
$bizId = iget('biz_id');
|
||||
$biz = $bizId ? qone("SELECT * FROM businesses WHERE id=?",[$bizId]) : null;
|
||||
$menuImagesEnabled = max(0, min(1, (int)setting('menu_item_image_limit','1'))) > 0;
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
@@ -31,15 +33,51 @@ if (isPost()) {
|
||||
$sid = (int)p('section_id');
|
||||
$name = ps('item_name');
|
||||
if ($sid && $name) {
|
||||
$imagePath = '';
|
||||
if ($menuImagesEnabled) {
|
||||
$upload = storeSingleUpload(
|
||||
'item_image',
|
||||
'menus',
|
||||
imageExts(),
|
||||
mbToBytes(setting('menu_upload_max_mb','5'), 5),
|
||||
[
|
||||
'image_only' => true,
|
||||
'resize_over_bytes' => mbToBytes(setting('forum_image_resize_threshold_mb','1'), 1),
|
||||
'image_max_width' => (int)setting('forum_image_max_width','1600'),
|
||||
'image_quality' => (int)setting('forum_image_quality','82'),
|
||||
]
|
||||
);
|
||||
if (!$upload['ok']) { flash($upload['error'], 'error'); go("/admin/menus.php?biz_id=$bizId"); }
|
||||
$imagePath = (string)($upload['path'] ?? '');
|
||||
}
|
||||
$ord = (int)qval("SELECT COALESCE(MAX(sort_order),0)+1 FROM menu_items WHERE section_id=?",[$sid]);
|
||||
qrun("INSERT INTO menu_items(section_id,name,description,price,is_available,sort_order)VALUES(?,?,?,?,?,?)",[$sid,$name,ps('item_desc'),(float)p('item_price',0),(int)p('is_available',1),$ord]);
|
||||
qrun("INSERT INTO menu_items(section_id,name,description,price,image_path,is_available,sort_order)VALUES(?,?,?,?,?,?,?)",[$sid,$name,ps('item_desc'),(float)p('item_price',0),$imagePath,(int)p('is_available',1),$ord]);
|
||||
flash('Item added!','success');
|
||||
}
|
||||
go("/admin/menus.php?biz_id=$bizId");
|
||||
}
|
||||
if ($act === 'edit_item') {
|
||||
$iid = (int)p('iid');
|
||||
qrun("UPDATE menu_items SET name=?,description=?,price=?,is_available=?,sort_order=? WHERE id=?",[ps('item_name'),ps('item_desc'),(float)p('item_price',0),(int)p('is_available',1),(int)p('item_order'),$iid]);
|
||||
$item = qone("SELECT * FROM menu_items WHERE id=?", [$iid]);
|
||||
$imagePath = (string)($item['image_path'] ?? '');
|
||||
if ($menuImagesEnabled && p('remove_item_image','0') === '1') $imagePath = '';
|
||||
if ($menuImagesEnabled) {
|
||||
$upload = storeSingleUpload(
|
||||
'item_image',
|
||||
'menus',
|
||||
imageExts(),
|
||||
mbToBytes(setting('menu_upload_max_mb','5'), 5),
|
||||
[
|
||||
'image_only' => true,
|
||||
'resize_over_bytes' => mbToBytes(setting('forum_image_resize_threshold_mb','1'), 1),
|
||||
'image_max_width' => (int)setting('forum_image_max_width','1600'),
|
||||
'image_quality' => (int)setting('forum_image_quality','82'),
|
||||
]
|
||||
);
|
||||
if (!$upload['ok']) { flash($upload['error'], 'error'); go("/admin/menus.php?biz_id=$bizId"); }
|
||||
if (!empty($upload['path'])) $imagePath = (string)$upload['path'];
|
||||
}
|
||||
qrun("UPDATE menu_items SET name=?,description=?,price=?,image_path=?,is_available=?,sort_order=? WHERE id=?",[ps('item_name'),ps('item_desc'),(float)p('item_price',0),$imagePath,(int)p('is_available',1),(int)p('item_order'),$iid]);
|
||||
flash('Item updated!','success'); go("/admin/menus.php?biz_id=$bizId");
|
||||
}
|
||||
if ($act === 'del_item') {
|
||||
@@ -55,6 +93,7 @@ if ($biz) {
|
||||
$items = [];
|
||||
foreach ($sections as $s) $items[$s['id']] = qall("SELECT * FROM menu_items WHERE section_id=? ORDER BY sort_order",[$s['id']]);
|
||||
}
|
||||
include __DIR__.'/admin_header.php';
|
||||
?>
|
||||
<?php adminTitle('Manage Menus','Edit menus, sections, and items for any business.') ?>
|
||||
|
||||
@@ -117,11 +156,12 @@ if ($biz) {
|
||||
<?php if($items[$sec['id']]): ?>
|
||||
<div class="tbl-wrap" style="margin-bottom:.85rem">
|
||||
<table class="dtbl">
|
||||
<thead><tr><th>ITEM NAME</th><th>DESCRIPTION</th><th>PRICE</th><th>AVAIL.</th><th>ORDER</th><th></th></tr></thead>
|
||||
<thead><tr><th>ITEM NAME</th><th>IMAGE</th><th>DESCRIPTION</th><th>PRICE</th><th>AVAIL.</th><th>ORDER</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($items[$sec['id']] as $item): ?>
|
||||
<tr>
|
||||
<td class="td-name"><?=e($item['name'])?></td>
|
||||
<td><?php if(!empty($item['image_path'])): ?><img src="<?=e($item['image_path'])?>" alt="<?=e($item['name'])?>" class="admin-menu-thumb"><?php else: ?>—<?php endif; ?></td>
|
||||
<td style="font-size:.8rem;max-width:180px"><?=e(substr($item['description'],0,80))?></td>
|
||||
<td style="color:var(--gold);font-family:'Oswald',sans-serif"><?=$item['price']>0?money($item['price']):'—'?></td>
|
||||
<td><?=$item['is_available']?'<span class="badge badge-green">Yes</span>':'<span class="badge badge-gray">No</span>'?></td>
|
||||
@@ -132,12 +172,25 @@ if ($biz) {
|
||||
<form method="POST" style="display:inline"><?=csrfField()?><input type="hidden" name="_act" value="del_item"><input type="hidden" name="iid" value="<?=$item['id']?>"><button class="btn btn-xs btn-danger" data-confirm="Delete item?">🗑️</button></form>
|
||||
</div>
|
||||
<div id="eif<?=$item['id']?>" class="hidden" style="background:var(--bg2);border:1px solid var(--bdr);border-radius:4px;padding:.75rem;margin-top:.4rem;min-width:320px">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="edit_item"><input type="hidden" name="iid" value="<?=$item['id']?>">
|
||||
<form method="POST" enctype="multipart/form-data"><?=csrfField()?><input type="hidden" name="_act" value="edit_item"><input type="hidden" name="iid" value="<?=$item['id']?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">NAME *</label><input type="text" name="item_name" class="finput" value="<?=e($item['name'])?>" required></div>
|
||||
<div class="fg"><label class="flabel">PRICE</label><input type="number" name="item_price" class="finput" value="<?=$item['price']?>" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" value="<?=e($item['description'])?>"></div>
|
||||
<?php if($menuImagesEnabled): ?>
|
||||
<div class="fg">
|
||||
<label class="flabel">ITEM IMAGE</label>
|
||||
<?php if(!empty($item['image_path'])): ?>
|
||||
<div class="menu-admin-image">
|
||||
<img src="<?=e($item['image_path'])?>" alt="<?=e($item['name'])?>">
|
||||
<label style="display:flex;align-items:center;gap:.45rem;font-size:.82rem;color:var(--text-m);cursor:pointer"><input type="checkbox" name="remove_item_image" value="1"> Remove current image</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="item_image" class="finput" accept="image/*">
|
||||
<div class="fhint">One image allowed. Max <?=e(setting('menu_upload_max_mb','5'))?> MB.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">AVAILABLE</label><select name="is_available" class="fselect"><option value="1"<?=$item['is_available']?' selected':''?>>Yes</option><option value="0"<?=!$item['is_available']?' selected':''?>>No</option></select></div>
|
||||
<div class="fg"><label class="flabel">SORT ORDER</label><input type="number" name="item_order" class="finput" value="<?=$item['sort_order']?>"></div>
|
||||
@@ -156,12 +209,15 @@ if ($biz) {
|
||||
<!-- Add item -->
|
||||
<details><summary style="cursor:pointer;font-size:.82rem;color:var(--gold);font-family:'Oswald',sans-serif;letter-spacing:.08em">➕ Add Item to "<?=e($sec['name'])?>"</summary>
|
||||
<div style="padding:.75rem 0">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add_item"><input type="hidden" name="section_id" value="<?=$sec['id']?>">
|
||||
<form method="POST" enctype="multipart/form-data"><?=csrfField()?><input type="hidden" name="_act" value="add_item"><input type="hidden" name="section_id" value="<?=$sec['id']?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">ITEM NAME *</label><input type="text" name="item_name" class="finput" required placeholder="e.g. Spaghetti Carbonara"></div>
|
||||
<div class="fg"><label class="flabel">PRICE ($)</label><input type="number" name="item_price" class="finput" value="0" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" placeholder="Brief description…"></div>
|
||||
<?php if($menuImagesEnabled): ?>
|
||||
<div class="fg"><label class="flabel">ITEM IMAGE</label><input type="file" name="item_image" class="finput" accept="image/*"><div class="fhint">Optional. One image allowed, max <?=e(setting('menu_upload_max_mb','5'))?> MB.</div></div>
|
||||
<?php endif; ?>
|
||||
<div class="fg"><label class="flabel">AVAILABLE</label><select name="is_available" class="fselect"><option value="1">Yes</option><option value="0">No</option></select></div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add Item</button>
|
||||
</form>
|
||||
|
||||
+59
-1
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
requireAdmin();
|
||||
$adminTitle = 'Site Settings';
|
||||
include __DIR__.'/admin_header.php';
|
||||
|
||||
if (isPost()) {
|
||||
csrfCheck();
|
||||
@@ -13,6 +14,15 @@ if (isPost()) {
|
||||
setSetting('registration_enabled',p('registration_enabled','0'));
|
||||
setSetting('events_require_approval',p('events_require_approval','0'));
|
||||
setSetting('home_random_interval',p('home_random_interval','2hours') === 'day' ? 'day' : '2hours');
|
||||
if (array_key_exists('event_upload_max_mb', $_POST)) {
|
||||
setSetting('event_upload_max_mb', (string)max(1, (float)p('event_upload_max_mb','5')));
|
||||
setSetting('menu_item_image_limit', (string)max(0, min(1, (int)p('menu_item_image_limit','1'))));
|
||||
setSetting('menu_upload_max_mb', (string)max(1, (float)p('menu_upload_max_mb','5')));
|
||||
setSetting('avatar_upload_max_mb', (string)max(1, (float)p('avatar_upload_max_mb','3')));
|
||||
setSetting('forum_image_resize_threshold_mb', (string)max(0.5, (float)p('forum_image_resize_threshold_mb','1')));
|
||||
setSetting('forum_image_max_width', (string)max(640, min(3200, (int)p('forum_image_max_width','1600'))));
|
||||
setSetting('forum_image_quality', (string)max(40, min(95, (int)p('forum_image_quality','82'))));
|
||||
}
|
||||
if (array_key_exists('mailgun_domain', $_POST)) {
|
||||
setSetting('site_base_url', rtrim(ps('site_base_url'), '/'));
|
||||
setSetting('mailgun_region', p('mailgun_region','us') === 'eu' ? 'eu' : 'us');
|
||||
@@ -55,7 +65,15 @@ $mailgunFromName = setting('mailgun_from_name','Discover Keyser WV');
|
||||
$mailgunFromEmail = setting('mailgun_from_email','');
|
||||
$mailgunSendMode = setting('mailgun_send_mode','html');
|
||||
$mailgunHasKey = setting('mailgun_api_key','') !== '';
|
||||
$eventUploadMax = setting('event_upload_max_mb','5');
|
||||
$menuImageLimit = setting('menu_item_image_limit','1');
|
||||
$menuUploadMax = setting('menu_upload_max_mb','5');
|
||||
$avatarUploadMax = setting('avatar_upload_max_mb','3');
|
||||
$resizeThreshold = setting('forum_image_resize_threshold_mb','1');
|
||||
$resizeMaxWidth = setting('forum_image_max_width','1600');
|
||||
$resizeQuality = setting('forum_image_quality','82');
|
||||
$me = qone("SELECT * FROM users WHERE id=?",[uid()]);
|
||||
include __DIR__.'/admin_header.php';
|
||||
?>
|
||||
<?php adminTitle('Site Settings') ?>
|
||||
|
||||
@@ -119,6 +137,39 @@ $me = qone("SELECT * FROM users WHERE id=?",[uid()]);
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">🖼️ UPLOAD & IMAGE SETTINGS</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="settings">
|
||||
<input type="hidden" name="site_name" value="<?=e($siteName)?>">
|
||||
<input type="hidden" name="site_tagline" value="<?=e($siteTagline)?>">
|
||||
<input type="hidden" name="contact_email" value="<?=e($contactEmail)?>">
|
||||
<input type="hidden" name="registration_enabled" value="<?=e($regEnabled)?>">
|
||||
<input type="hidden" name="events_require_approval" value="<?=e($evtApproval)?>">
|
||||
<input type="hidden" name="home_random_interval" value="<?=e($homeRandomInterval)?>">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">EVENT IMAGE MAX MB</label><input type="number" name="event_upload_max_mb" class="finput" min="1" step="0.5" value="<?=e($eventUploadMax)?>"></div>
|
||||
<div class="fg"><label class="flabel">AVATAR MAX MB</label><input type="number" name="avatar_upload_max_mb" class="finput" min="1" step="0.5" value="<?=e($avatarUploadMax)?>"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg">
|
||||
<label class="flabel">MENU ITEM IMAGE LIMIT</label>
|
||||
<select name="menu_item_image_limit" class="fselect">
|
||||
<option value="1"<?=$menuImageLimit==='1'?' selected':''?>>One image per item</option>
|
||||
<option value="0"<?=$menuImageLimit==='0'?' selected':''?>>No menu item images</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">MENU IMAGE MAX MB</label><input type="number" name="menu_upload_max_mb" class="finput" min="1" step="0.5" value="<?=e($menuUploadMax)?>"></div>
|
||||
</div>
|
||||
<hr class="divider" style="margin:1rem 0">
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">RESIZE IMAGES OVER MB</label><input type="number" name="forum_image_resize_threshold_mb" class="finput" min="0.5" step="0.5" value="<?=e($resizeThreshold)?>"></div>
|
||||
<div class="fg"><label class="flabel">MAX IMAGE WIDTH</label><input type="number" name="forum_image_max_width" class="finput" min="640" max="3200" step="80" value="<?=e($resizeMaxWidth)?>"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">IMAGE QUALITY</label><input type="number" name="forum_image_quality" class="finput" min="40" max="95" value="<?=e($resizeQuality)?>"><div class="fhint">Used when large uploaded images are re-saved.</div></div>
|
||||
<button type="submit" class="btn btn-primary">Save Upload Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">✉️ EMAIL VERIFICATION & MAILGUN</div>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="settings">
|
||||
@@ -128,6 +179,13 @@ $me = qone("SELECT * FROM users WHERE id=?",[uid()]);
|
||||
<input type="hidden" name="registration_enabled" value="<?=e($regEnabled)?>">
|
||||
<input type="hidden" name="events_require_approval" value="<?=e($evtApproval)?>">
|
||||
<input type="hidden" name="home_random_interval" value="<?=e($homeRandomInterval)?>">
|
||||
<input type="hidden" name="event_upload_max_mb" value="<?=e($eventUploadMax)?>">
|
||||
<input type="hidden" name="menu_item_image_limit" value="<?=e($menuImageLimit)?>">
|
||||
<input type="hidden" name="menu_upload_max_mb" value="<?=e($menuUploadMax)?>">
|
||||
<input type="hidden" name="avatar_upload_max_mb" value="<?=e($avatarUploadMax)?>">
|
||||
<input type="hidden" name="forum_image_resize_threshold_mb" value="<?=e($resizeThreshold)?>">
|
||||
<input type="hidden" name="forum_image_max_width" value="<?=e($resizeMaxWidth)?>">
|
||||
<input type="hidden" name="forum_image_quality" value="<?=e($resizeQuality)?>">
|
||||
|
||||
<div class="fg">
|
||||
<label class="flabel">SITE BASE URL</label>
|
||||
|
||||
+54
-2
@@ -356,6 +356,13 @@ address{font-style:normal}
|
||||
.evt-meta span{display:flex;align-items:center;gap:.3rem}
|
||||
.evt-desc{font-size:.88rem;color:var(--text-m);line-height:1.62}
|
||||
.evt-actions{margin-top:.75rem;display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.evt-image-wrap{width:180px;min-height:126px;align-self:stretch;flex:0 0 180px;overflow:hidden;border-radius:var(--r);border:1px solid rgba(38,244,255,.2);background:var(--bg4)}
|
||||
.evt-image{width:100%;height:100%;object-fit:cover}
|
||||
.event-strip .evt-card{display:grid;grid-template-columns:auto 1fr;align-items:start}
|
||||
.event-strip .evt-image-wrap{grid-column:1/-1;width:100%;height:150px;min-height:150px}
|
||||
.event-admin-image,.menu-admin-image{display:flex;align-items:center;gap:.85rem;margin-bottom:.65rem;flex-wrap:wrap}
|
||||
.event-admin-image img{width:150px;height:88px;object-fit:cover;border-radius:var(--r);border:1px solid var(--bdr-l)}
|
||||
.menu-admin-image img,.admin-menu-thumb{width:72px;height:72px;object-fit:cover;border-radius:var(--r);border:1px solid var(--bdr-l)}
|
||||
|
||||
/* ── About / Facts ──────────────────────────────────────── */
|
||||
.fact-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:1.1rem}
|
||||
@@ -409,6 +416,51 @@ address{font-style:normal}
|
||||
.text-gold{color:var(--gold)}.text-muted{color:var(--text-m)}.text-dim{color:var(--text-d)}
|
||||
.text-right{text-align:right}.text-center{text-align:center}
|
||||
|
||||
/* ── Avatars / Profile Images ───────────────────────────── */
|
||||
.avatar{display:inline-grid;place-items:center;border-radius:50%;object-fit:cover;flex-shrink:0;border:1px solid rgba(38,244,255,.32);background:linear-gradient(135deg,rgba(38,244,255,.18),rgba(255,79,216,.14));color:var(--cyan-l);font-family:'Oswald',sans-serif;font-weight:600;letter-spacing:.04em;box-shadow:0 0 14px rgba(38,244,255,.08)}
|
||||
.avatar-xs{width:24px;height:24px;font-size:.7rem}
|
||||
.avatar-sm{width:38px;height:38px;font-size:.9rem}
|
||||
.avatar-lg{width:96px;height:96px;font-size:2rem}
|
||||
.account-avatar-preview{display:flex;justify-content:center;margin:.25rem 0 1rem}
|
||||
.avatar-upload-grid{display:grid;grid-template-columns:240px 1fr;gap:1rem;align-items:start}
|
||||
.avatar-canvas{width:240px;height:240px;border-radius:50%;border:1px solid rgba(38,244,255,.34);background:var(--bg4);box-shadow:0 0 22px rgba(38,244,255,.08)}
|
||||
.avatar-crop-controls{display:grid;gap:.5rem;margin-top:.85rem}
|
||||
.avatar-crop-controls label{display:grid;gap:.2rem;font-family:'Oswald',sans-serif;font-size:.68rem;letter-spacing:.1em;color:var(--text-d)}
|
||||
.avatar-crop-controls input[type=range]{width:100%;accent-color:var(--cyan)}
|
||||
.rev-user{display:flex;align-items:center;gap:.7rem}
|
||||
|
||||
/* ── Menu images ────────────────────────────────────────── */
|
||||
.menu-item{gap:.9rem}
|
||||
.mi-image{width:86px;height:86px;object-fit:cover;border-radius:var(--r);border:1px solid rgba(38,244,255,.2);background:var(--bg4);flex-shrink:0}
|
||||
.menu-owner-row{display:flex;gap:.75rem;align-items:flex-start;min-width:0}
|
||||
.menu-owner-thumb{width:58px;height:58px;object-fit:cover;border-radius:var(--r);border:1px solid rgba(38,244,255,.22);flex-shrink:0}
|
||||
|
||||
/* ── Forum ──────────────────────────────────────────────── */
|
||||
.forum-layout{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:2rem;align-items:start}
|
||||
.forum-topic-list{display:grid;gap:.8rem}
|
||||
.forum-topic-row{display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:.85rem;align-items:center;padding:1rem;background:linear-gradient(180deg,rgba(18,31,56,.86),rgba(9,15,30,.92));border:1px solid rgba(38,244,255,.16);border-radius:var(--r);color:var(--text);transition:all var(--t)}
|
||||
.forum-topic-row:hover{border-color:rgba(38,244,255,.44);box-shadow:var(--sh-sm);transform:translateY(-1px)}
|
||||
.forum-topic-main{min-width:0}
|
||||
.forum-topic-main strong{display:block;color:var(--text);font-size:1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.forum-topic-main em{display:block;color:var(--text-d);font-style:normal;font-size:.8rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.forum-topic-count{font-family:'Oswald',sans-serif;letter-spacing:.08em;font-size:.74rem;color:var(--cyan-l);white-space:nowrap}
|
||||
.forum-byline{display:flex;align-items:center;gap:.45rem;color:var(--text-d);font-size:.86rem;flex-wrap:wrap}
|
||||
.forum-thread{max-width:980px}
|
||||
.forum-post,.forum-reply{background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r);padding:1.25rem;margin-bottom:1rem}
|
||||
.forum-post-body{color:var(--text-m);line-height:1.75;overflow-wrap:anywhere}
|
||||
.forum-attachments{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:.75rem;margin-top:1rem}
|
||||
.forum-attachment-image{display:block;overflow:hidden;border-radius:var(--r);border:1px solid rgba(38,244,255,.2);background:var(--bg4);aspect-ratio:4/3}
|
||||
.forum-attachment-image img{width:100%;height:100%;object-fit:cover;transition:transform var(--t)}
|
||||
.forum-attachment-image:hover img{transform:scale(1.03)}
|
||||
.forum-file{display:flex;justify-content:space-between;gap:.65rem;align-items:center;padding:.75rem .9rem;border:1px solid rgba(38,244,255,.18);border-radius:var(--r);background:rgba(18,31,56,.75);color:var(--text-m);min-width:0}
|
||||
.forum-file span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.forum-file em{font-style:normal;color:var(--text-d);font-size:.74rem;white-space:nowrap}
|
||||
.forum-replies-head{margin:2rem 0 1rem}
|
||||
.forum-reply-meta{display:flex;justify-content:space-between;align-items:center;gap:1rem;margin-bottom:.8rem;flex-wrap:wrap}
|
||||
.forum-reply-author{display:flex;align-items:center;gap:.65rem;font-family:'Oswald',sans-serif;letter-spacing:.06em;color:var(--text)}
|
||||
.forum-permalink{font-family:'Oswald',sans-serif;font-size:.72rem;letter-spacing:.08em;color:var(--cyan-l)}
|
||||
.admin-inline-editor{display:grid;gap:.45rem;min-width:320px;background:var(--bg2);border:1px solid var(--bdr);border-radius:var(--r);padding:.75rem;margin-top:.5rem}
|
||||
|
||||
/* ── Animations ─────────────────────────────────────────── */
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes fadeDown{from{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}
|
||||
@@ -416,13 +468,13 @@ address{font-style:normal}
|
||||
|
||||
/* ── Responsive ─────────────────────────────────────────── */
|
||||
@media(max-width:1100px){.footer-grid{grid-template-columns:1fr 1fr}.home-hero-grid{grid-template-columns:1fr}.hero-signal{grid-template-columns:1fr 1fr}.signal-card-main{min-height:auto}.about-split{grid-template-columns:1fr}}
|
||||
@media(max-width:900px){.detail-body{grid-template-columns:1fr}.admin-wrap{grid-template-columns:1fr}.admin-side{position:static;max-height:none;border-right:none;border-bottom:1px solid var(--bdr)}.form-row{grid-template-columns:1fr}.section-head{align-items:flex-start;flex-direction:column}.random-list{grid-template-columns:1fr}.signup-grid,.tier-grid{grid-template-columns:1fr}.home-facts{grid-template-columns:1fr}}
|
||||
@media(max-width:900px){.detail-body{grid-template-columns:1fr}.admin-wrap{grid-template-columns:1fr}.admin-side{position:static;max-height:none;border-right:none;border-bottom:1px solid var(--bdr)}.form-row{grid-template-columns:1fr}.section-head{align-items:flex-start;flex-direction:column}.random-list{grid-template-columns:1fr}.signup-grid,.tier-grid{grid-template-columns:1fr}.home-facts{grid-template-columns:1fr}.forum-layout{grid-template-columns:1fr}.avatar-upload-grid{grid-template-columns:1fr}.evt-image-wrap{width:140px;flex-basis:140px}}
|
||||
@media(max-width:768px){
|
||||
.nav-links{display:none;position:absolute;top:64px;left:0;right:0;background:var(--bg0);border-bottom:1px solid var(--bdr-l);flex-direction:column;padding:.75rem 1rem;gap:.1rem;box-shadow:var(--sh)}
|
||||
.nav-links.open{display:flex}.hamburger{display:flex}
|
||||
.user-wrap{width:100%}.user-btn{width:100%;text-align:left}
|
||||
.user-dd{position:static;box-shadow:none;border:none;background:var(--bg2)}
|
||||
.hero{min-height:80vh}.g3,.g4{grid-template-columns:1fr}.evt-card{flex-direction:column}
|
||||
.hero{min-height:80vh}.g3,.g4{grid-template-columns:1fr}.evt-card{flex-direction:column}.evt-image-wrap{width:100%;height:190px;flex-basis:auto}.event-strip .evt-card{display:flex}.event-strip .evt-image-wrap{height:190px}
|
||||
.home-hero{min-height:auto;padding:4rem 1rem 3rem}.home-hero .hero-h1{font-size:clamp(3rem,17vw,4.6rem)}.hero-search{flex-direction:column}.hero-search .s-icon{top:1.35rem}.hero-search .btn{justify-content:center}.hero-signal,.signal-mini-grid{grid-template-columns:1fr}.join-band{align-items:flex-start;flex-direction:column;margin-bottom:2.5rem}.signup-shell{padding:3rem 1rem}.signup-card{padding:1rem}.tier-card{min-height:auto}
|
||||
}
|
||||
@media(max-width:560px){.hero-h1{font-size:3rem}.footer-grid{grid-template-columns:1fr}.hrs-grid{grid-template-columns:1fr}.form-box{margin:2rem 1rem}.random-row{grid-template-columns:36px 1fr}.random-arrow{display:none}.home-hero .hero-eyebrow{letter-spacing:.18em}.btn{white-space:normal;text-align:center}.signal-number{font-size:3.6rem}}
|
||||
|
||||
+8
-2
@@ -136,7 +136,7 @@ $pageTitle = e($b['name']).' — Keyser, WV';
|
||||
$activeNav = 'dir';
|
||||
|
||||
$reviews = qall("
|
||||
SELECT r.*, u.username
|
||||
SELECT r.*, u.username, u.avatar_path
|
||||
FROM reviews r JOIN users u ON u.id = r.user_id
|
||||
WHERE r.business_id = ? AND r.is_approved = 1
|
||||
ORDER BY r.created_at DESC
|
||||
@@ -403,6 +403,9 @@ include __DIR__.'/includes/header.php';
|
||||
<?php endif; ?>
|
||||
<?php foreach ($mitems[$s['id']] as $mi): ?>
|
||||
<div class="menu-item">
|
||||
<?php if (!empty($mi['image_path'])): ?>
|
||||
<img src="<?=e($mi['image_path'])?>" alt="<?=e($mi['name'] ?? 'Menu item')?>" class="mi-image">
|
||||
<?php endif; ?>
|
||||
<div class="mi-info">
|
||||
<div class="mi-name"><?=e($mi['name'] ?? '')?></div>
|
||||
<?php if (!empty($mi['description'])): ?>
|
||||
@@ -461,9 +464,12 @@ include __DIR__.'/includes/header.php';
|
||||
<?php foreach ($reviews as $r): ?>
|
||||
<div class="rev-card">
|
||||
<div class="rev-hdr">
|
||||
<div>
|
||||
<div class="rev-user">
|
||||
<?=userAvatarHtml($r, 'avatar-sm')?>
|
||||
<div>
|
||||
<div class="rev-author"><?=e($r['username'] ?? 'Anonymous')?></div>
|
||||
<?=starDisplay((float)($r['rating'] ?? 0))?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row" style="gap:.5rem">
|
||||
<span class="rev-date"><?=ago($r['created_at'] ?? '')?></span>
|
||||
|
||||
+28
-3
@@ -12,10 +12,27 @@ if(isPost()&&ps('_act')==='submit_event'){
|
||||
$cost=ps('cost');$web=ps('website');$cn=ps('contact_name');$ce=ps('contact_email');$cp=ps('contact_phone');
|
||||
if(!$title) $errors[]='Event title is required.';
|
||||
if(!$date) $errors[]='Event date is required.';
|
||||
$imagePath = '';
|
||||
if(!$errors){
|
||||
$upload = storeSingleUpload(
|
||||
'event_image',
|
||||
'events',
|
||||
imageExts(),
|
||||
mbToBytes(setting('event_upload_max_mb','5'), 5),
|
||||
[
|
||||
'image_only' => true,
|
||||
'resize_over_bytes' => mbToBytes(setting('forum_image_resize_threshold_mb','1'), 1),
|
||||
'image_max_width' => (int)setting('forum_image_max_width','1600'),
|
||||
'image_quality' => (int)setting('forum_image_quality','82'),
|
||||
]
|
||||
);
|
||||
if (!$upload['ok']) $errors[] = $upload['error'];
|
||||
else $imagePath = (string)($upload['path'] ?? '');
|
||||
}
|
||||
if(!$errors){
|
||||
$status=setting('events_require_approval','1')==='1'?'pending':'approved';
|
||||
qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,submitted_by,status)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
[$title,$desc,$venue,$addr,$date,$time,$edate?:null,$etime,$cost,$web,$cn,$ce,$cp,uid(),$status]);
|
||||
qrun("INSERT INTO events(title,description,venue,address,event_date,event_time,end_date,end_time,cost,website,contact_name,contact_email,contact_phone,image_path,submitted_by,status)VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
[$title,$desc,$venue,$addr,$date,$time,$edate?:null,$etime,$cost,$web,$cn,$ce,$cp,$imagePath,uid(),$status]);
|
||||
flash($status==='approved'?'Event submitted and published!':'Event submitted! It will appear after review.','success');
|
||||
go('/events.php');
|
||||
}
|
||||
@@ -48,7 +65,7 @@ include __DIR__.'/includes/header.php';
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">📅 SUBMIT A COMMUNITY EVENT</div>
|
||||
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="submit_event">
|
||||
<form method="POST" enctype="multipart/form-data"><?=csrfField()?><input type="hidden" name="_act" value="submit_event">
|
||||
<div class="fg"><label class="flabel">EVENT TITLE *</label><input type="text" name="title" class="finput" value="<?=e($_POST['title']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><textarea name="description" class="ftextarea" style="min-height:110px"><?=e($_POST['description']??'')?></textarea></div>
|
||||
<div class="form-row">
|
||||
@@ -67,6 +84,11 @@ include __DIR__.'/includes/header.php';
|
||||
<div class="fg"><label class="flabel">COST / ADMISSION</label><input type="text" name="cost" class="finput" value="<?=e($_POST['cost']??'Free')?>" placeholder="Free, $10, Varies…"></div>
|
||||
<div class="fg"><label class="flabel">WEBSITE</label><input type="text" name="website" class="finput" value="<?=e($_POST['website']??'')?>" placeholder="example.com"></div>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label class="flabel">EVENT IMAGE</label>
|
||||
<input type="file" name="event_image" class="finput" accept="image/*">
|
||||
<div class="fhint">Optional JPG, PNG, GIF, or WebP. Max <?=e(setting('event_upload_max_mb','5'))?> MB.</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="fg"><label class="flabel">CONTACT NAME</label><input type="text" name="contact_name" class="finput" value="<?=e($_POST['contact_name']??'')?>"></div>
|
||||
<div class="fg"><label class="flabel">CONTACT PHONE</label><input type="text" name="contact_phone" class="finput" value="<?=e($_POST['contact_phone']??'')?>"></div>
|
||||
@@ -87,6 +109,9 @@ include __DIR__.'/includes/header.php';
|
||||
<?php if($events): ?>
|
||||
<?php foreach($events as $ev): ?>
|
||||
<div class="evt-card">
|
||||
<?php if(!empty($ev['image_path'])): ?>
|
||||
<div class="evt-image-wrap"><img src="<?=e($ev['image_path'])?>" alt="<?=e($ev['title'])?>" class="evt-image"></div>
|
||||
<?php endif; ?>
|
||||
<div class="evt-date-box">
|
||||
<div class="evt-mon"><?=strtoupper(date('M',strtotime($ev['event_date'])))?></div>
|
||||
<div class="evt-day"><?=date('j',strtotime($ev['event_date']))?></div>
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
|
||||
$activeNav = 'forum';
|
||||
$pageTitle = 'Community Forum - Keyser, WV';
|
||||
|
||||
if (setting('forum_enabled','1') !== '1' && !isAdmin()) {
|
||||
flash('The community forum is currently offline.', 'warning');
|
||||
go('/index.php');
|
||||
}
|
||||
requireLogin('/forum');
|
||||
|
||||
$topicSlug = gs('topic');
|
||||
$path = (string)(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '');
|
||||
if ($topicSlug === '' && preg_match('#^/forum/([^/]+)/?$#', $path, $m)) {
|
||||
$topicSlug = urldecode($m[1]);
|
||||
}
|
||||
|
||||
function forumAllowedExts(): array {
|
||||
return uploadAllowedExts(setting('forum_allowed_filetypes','jpg,jpeg,png,gif,webp,pdf,txt,doc,docx'), ['jpg','jpeg','png','gif','webp','pdf','txt','doc','docx']);
|
||||
}
|
||||
function forumUploadOpts(): array {
|
||||
return [
|
||||
'resize_over_bytes' => mbToBytes(setting('forum_image_resize_threshold_mb','1'), 1),
|
||||
'image_max_width' => (int)setting('forum_image_max_width','1600'),
|
||||
'image_quality' => (int)setting('forum_image_quality','82'),
|
||||
];
|
||||
}
|
||||
function forumStoreAttachments(): array {
|
||||
return storeMultipleUploads(
|
||||
'attachments',
|
||||
'forum',
|
||||
forumAllowedExts(),
|
||||
mbToBytes(setting('forum_upload_max_mb','5'), 5),
|
||||
forumUploadOpts()
|
||||
);
|
||||
}
|
||||
function forumSaveAttachments(array $files, int $userId, int $topicId = 0, int $replyId = 0): void {
|
||||
foreach ($files as $file) {
|
||||
qrun(
|
||||
"INSERT INTO forum_attachments(topic_id,reply_id,user_id,file_path,original_name,mime_type,file_size)VALUES(?,?,?,?,?,?,?)",
|
||||
[$topicId ?: null, $replyId ?: null, $userId, $file['path'], $file['name'] ?? '', $file['mime'] ?? '', (int)($file['size'] ?? 0)]
|
||||
);
|
||||
}
|
||||
}
|
||||
function forumAttachmentList(array $attachments): string {
|
||||
if (!$attachments) return '';
|
||||
$out = '<div class="forum-attachments">';
|
||||
foreach ($attachments as $att) {
|
||||
$name = $att['original_name'] ?: basename((string)$att['file_path']);
|
||||
if (isImagePath((string)$att['file_path'])) {
|
||||
$out .= '<a href="'.e($att['file_path']).'" class="forum-attachment-image" target="_blank" rel="noopener"><img src="'.e($att['file_path']).'" alt="'.e($name).'"></a>';
|
||||
} else {
|
||||
$out .= '<a href="'.e($att['file_path']).'" class="forum-file" target="_blank" rel="noopener"><span>'.e($name).'</span><em>'.formatBytes((int)$att['file_size']).'</em></a>';
|
||||
}
|
||||
}
|
||||
return $out.'</div>';
|
||||
}
|
||||
function forumText(string $text): string {
|
||||
return nl2br(e($text));
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$allowedHint = implode(', ', forumAllowedExts());
|
||||
$maxHint = setting('forum_upload_max_mb','5').' MB';
|
||||
|
||||
if ($topicSlug === '') {
|
||||
if (isPost() && ps('_act') === 'create_topic') {
|
||||
csrfCheck();
|
||||
$title = ps('title');
|
||||
$body = ps('body');
|
||||
$catId = (int)p('category_id');
|
||||
$cat = qone("SELECT * FROM forum_categories WHERE id=? AND is_active=1", [$catId]);
|
||||
if ($title === '') $errors[] = 'Topic title is required.';
|
||||
if ($body === '') $errors[] = 'Topic body is required.';
|
||||
if (!$cat) $errors[] = 'Please choose a forum category.';
|
||||
|
||||
$uploaded = ['ok'=>true, 'files'=>[]];
|
||||
if (!$errors) {
|
||||
$uploaded = forumStoreAttachments();
|
||||
if (!$uploaded['ok']) $errors[] = $uploaded['error'];
|
||||
}
|
||||
if (!$errors) {
|
||||
$tempSlug = 'draft-'.bin2hex(random_bytes(8));
|
||||
$topicId = qrun(
|
||||
"INSERT INTO forum_topics(category_id,user_id,title,slug,body)VALUES(?,?,?,?,?)",
|
||||
[$catId, uid(), $title, $tempSlug, $body]
|
||||
);
|
||||
$slug = uniqueForumSlug($title, $topicId);
|
||||
qrun("UPDATE forum_topics SET slug=? WHERE id=?", [$slug, $topicId]);
|
||||
forumSaveAttachments($uploaded['files'] ?? [], uid(), $topicId, 0);
|
||||
flash('Topic posted.', 'success');
|
||||
go(forumTopicUrl(['slug'=>$slug]));
|
||||
}
|
||||
}
|
||||
|
||||
$categories = qall("SELECT * FROM forum_categories WHERE is_active=1 ORDER BY sort_order,name");
|
||||
$topics = qall("
|
||||
SELECT ft.*, fc.name category_name, u.username, u.avatar_path,
|
||||
(SELECT COUNT(*) FROM forum_replies fr WHERE fr.topic_id=ft.id) AS reply_count,
|
||||
COALESCE((SELECT MAX(fr.created_at) FROM forum_replies fr WHERE fr.topic_id=ft.id), ft.created_at) AS last_activity
|
||||
FROM forum_topics ft
|
||||
JOIN forum_categories fc ON fc.id=ft.category_id
|
||||
JOIN users u ON u.id=ft.user_id
|
||||
ORDER BY ft.is_pinned DESC, datetime(last_activity) DESC
|
||||
");
|
||||
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner" style="display:flex;justify-content:space-between;align-items:flex-end;gap:1rem;flex-wrap:wrap">
|
||||
<div>
|
||||
<div class="eyebrow">COMMUNITY FORUM</div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin-bottom:.4rem">Keyser Community Forum</h1>
|
||||
<p style="color:var(--text-m)">Topics and replies from registered members and business owners.</p>
|
||||
</div>
|
||||
<?php if(setting('forum_enabled','1') !== '1' && isAdmin()): ?><span class="badge badge-gold">Disabled for non-admins</span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section forum-layout">
|
||||
<div>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<div class="eyebrow">LATEST TOPICS</div>
|
||||
<h2 class="sec-title">Conversations</h2>
|
||||
</div>
|
||||
</div>
|
||||
<?php if($topics): ?>
|
||||
<div class="forum-topic-list">
|
||||
<?php foreach($topics as $topic): ?>
|
||||
<a href="<?=e(forumTopicUrl($topic))?>" class="forum-topic-row">
|
||||
<?=userAvatarHtml($topic, 'avatar-sm')?>
|
||||
<span class="forum-topic-main">
|
||||
<strong><?=e($topic['title'])?></strong>
|
||||
<em><?=e($topic['category_name'])?> by <?=e($topic['username'])?> · <?=ago($topic['last_activity'])?></em>
|
||||
</span>
|
||||
<span class="forum-topic-count"><?=$topic['reply_count']?> repl<?=$topic['reply_count']==1?'y':'ies'?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty"><div class="empty-ico">+</div><h3>No topics yet</h3><p style="margin-top:.5rem">Start the first community conversation.</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">START A TOPIC</div>
|
||||
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
|
||||
<?php if($categories): ?>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<?=csrfField()?><input type="hidden" name="_act" value="create_topic">
|
||||
<div class="fg"><label class="flabel">CATEGORY *</label><select name="category_id" class="fselect" required><option value="">Choose category</option><?php foreach($categories as $cat): ?><option value="<?=$cat['id']?>"<?=((int)($_POST['category_id']??0)===(int)$cat['id'])?' selected':''?>><?=e($cat['name'])?></option><?php endforeach; ?></select></div>
|
||||
<div class="fg"><label class="flabel">TITLE *</label><input type="text" name="title" class="finput" value="<?=e($_POST['title']??'')?>" required></div>
|
||||
<div class="fg"><label class="flabel">BODY *</label><textarea name="body" class="ftextarea" required><?=e($_POST['body']??'')?></textarea></div>
|
||||
<div class="fg"><label class="flabel">ATTACH FILES</label><input type="file" name="attachments[]" class="finput" multiple><div class="fhint">Allowed: <?=e($allowedHint)?>. Max <?=e($maxHint)?> each.</div></div>
|
||||
<button class="btn btn-primary btn-block">Post Topic</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<p style="color:var(--text-m)">No active categories are available yet.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<?php
|
||||
include __DIR__.'/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$topic = qone("
|
||||
SELECT ft.*, fc.name category_name, u.username, u.avatar_path
|
||||
FROM forum_topics ft
|
||||
JOIN forum_categories fc ON fc.id=ft.category_id
|
||||
JOIN users u ON u.id=ft.user_id
|
||||
WHERE ft.slug=?
|
||||
", [$topicSlug]);
|
||||
if (!$topic) { http_response_code(404); flash('Forum topic not found.', 'error'); go('/forum'); }
|
||||
|
||||
if (isPost() && ps('_act') === 'reply') {
|
||||
csrfCheck();
|
||||
if ((int)$topic['is_locked'] === 1 && !isAdmin()) {
|
||||
$errors[] = 'This topic is locked.';
|
||||
} else {
|
||||
$body = ps('body');
|
||||
$hasFile = false;
|
||||
foreach (normalizeUploadFiles('attachments') as $file) {
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) $hasFile = true;
|
||||
}
|
||||
if ($body === '' && !$hasFile) $errors[] = 'Add a reply or attach a file.';
|
||||
$uploaded = ['ok'=>true, 'files'=>[]];
|
||||
if (!$errors) {
|
||||
$uploaded = forumStoreAttachments();
|
||||
if (!$uploaded['ok']) $errors[] = $uploaded['error'];
|
||||
}
|
||||
if (!$errors) {
|
||||
$replyId = qrun("INSERT INTO forum_replies(topic_id,user_id,body)VALUES(?,?,?)", [$topic['id'], uid(), $body]);
|
||||
forumSaveAttachments($uploaded['files'] ?? [], uid(), 0, $replyId);
|
||||
qrun("UPDATE forum_topics SET updated_at=datetime('now') WHERE id=?", [$topic['id']]);
|
||||
flash('Reply posted.', 'success');
|
||||
go(forumTopicUrl($topic).'#reply-'.$replyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$topicAttachments = qall("SELECT * FROM forum_attachments WHERE topic_id=? AND reply_id IS NULL ORDER BY created_at", [$topic['id']]);
|
||||
$replies = qall("
|
||||
SELECT fr.*, u.username, u.avatar_path
|
||||
FROM forum_replies fr
|
||||
JOIN users u ON u.id=fr.user_id
|
||||
WHERE fr.topic_id=?
|
||||
ORDER BY fr.created_at ASC
|
||||
", [$topic['id']]);
|
||||
$replyAttachments = [];
|
||||
foreach ($replies as $reply) {
|
||||
$replyAttachments[$reply['id']] = qall("SELECT * FROM forum_attachments WHERE reply_id=? ORDER BY created_at", [$reply['id']]);
|
||||
}
|
||||
|
||||
include __DIR__.'/includes/header.php';
|
||||
?>
|
||||
<div class="page-hdr">
|
||||
<div class="page-hdr-inner">
|
||||
<div class="breadcrumb"><a href="/index.php">Home</a><span>›</span><a href="/forum">Forum</a><span>›</span><?=e($topic['title'])?></div>
|
||||
<div class="cat-badge"><?=e($topic['category_name'])?></div>
|
||||
<h1 style="font-size:clamp(2rem,4vw,2.9rem);margin:.45rem 0"><?=e($topic['title'])?></h1>
|
||||
<div class="forum-byline"><?=userAvatarHtml($topic, 'avatar-xs')?> Posted by <?=e($topic['username'])?> · <?=ago($topic['created_at'])?><?php if($topic['is_locked']): ?> · <span class="badge badge-gray">Locked</span><?php endif; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-sm forum-thread">
|
||||
<article class="forum-post">
|
||||
<div class="forum-post-body"><?=forumText($topic['body'])?></div>
|
||||
<?=forumAttachmentList($topicAttachments)?>
|
||||
</article>
|
||||
|
||||
<div class="forum-replies-head">
|
||||
<div class="eyebrow">REPLIES</div>
|
||||
<h2 class="sec-title"><?=count($replies)?> repl<?=count($replies)===1?'y':'ies'?></h2>
|
||||
</div>
|
||||
|
||||
<?php foreach($replies as $reply): ?>
|
||||
<article class="forum-reply" id="reply-<?=$reply['id']?>">
|
||||
<div class="forum-reply-meta">
|
||||
<div class="forum-reply-author"><?=userAvatarHtml($reply, 'avatar-sm')?><span><?=e($reply['username'])?></span></div>
|
||||
<a href="<?=e(forumTopicUrl($topic).'#reply-'.$reply['id'])?>" class="forum-permalink">#reply-<?=$reply['id']?></a>
|
||||
</div>
|
||||
<?php if($reply['body'] !== ''): ?><div class="forum-post-body"><?=forumText($reply['body'])?></div><?php endif; ?>
|
||||
<?=forumAttachmentList($replyAttachments[$reply['id']] ?? [])?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="ibox" style="border-color:var(--gold-d)">
|
||||
<div class="ibox-title">POST A REPLY</div>
|
||||
<?php if($errors): ?><div class="err-box"><?=implode('<br>',array_map('e',$errors))?></div><?php endif; ?>
|
||||
<?php if((int)$topic['is_locked'] === 1 && !isAdmin()): ?>
|
||||
<p style="color:var(--text-m)">This topic is locked.</p>
|
||||
<?php else: ?>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<?=csrfField()?><input type="hidden" name="_act" value="reply">
|
||||
<div class="fg"><label class="flabel">REPLY</label><textarea name="body" class="ftextarea"><?=e($_POST['body']??'')?></textarea></div>
|
||||
<div class="fg"><label class="flabel">ATTACH FILES</label><input type="file" name="attachments[]" class="finput" multiple><div class="fhint">Allowed: <?=e($allowedHint)?>. Max <?=e($maxHint)?> each.</div></div>
|
||||
<button class="btn btn-primary">Post Reply</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__.'/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
require dirname(__DIR__).'/forum.php';
|
||||
+137
-1
@@ -48,9 +48,18 @@ function _ensureAppUpgrades(PDO $db): void {
|
||||
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 (
|
||||
@@ -86,6 +95,51 @@ function _ensureAppUpgrades(PDO $db): void {
|
||||
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 = [
|
||||
@@ -97,9 +151,30 @@ function _ensureAppUpgrades(PDO $db): void {
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
@@ -119,6 +194,7 @@ function _schema(PDO $db): void {
|
||||
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
|
||||
);
|
||||
@@ -206,6 +282,7 @@ function _schema(PDO $db): void {
|
||||
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
|
||||
);
|
||||
@@ -246,6 +323,7 @@ function _schema(PDO $db): void {
|
||||
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,
|
||||
@@ -261,6 +339,47 @@ function _schema(PDO $db): void {
|
||||
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)
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
@@ -281,7 +400,17 @@ function _seed(PDO $db): void {
|
||||
'mailgun_api_key'=>'',
|
||||
'mailgun_from_name'=>'Discover Keyser WV',
|
||||
'mailgun_from_email'=>'',
|
||||
'mailgun_send_mode'=>'html'] as $k=>$v)
|
||||
'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" */
|
||||
@@ -299,6 +428,13 @@ function _seed(PDO $db): void {
|
||||
['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']);
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<?php if (setting('registration_enabled','1')==='1'): ?>
|
||||
<li><a href="/register.php">Create Account</a></li>
|
||||
<?php endif; ?>
|
||||
<?php if ((authed() && setting('forum_enabled','1')==='1') || isAdmin()): ?>
|
||||
<li><a href="/forum">Community Forum</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>
|
||||
|
||||
@@ -150,6 +150,233 @@ function validBusinessVideoUrl(string $url): bool {
|
||||
return in_array($scheme, ['http','https'], true) && businessVideoProvider($url) !== '';
|
||||
}
|
||||
|
||||
/* ── Uploads / Images ─────────────────────────────────── */
|
||||
function uploadRoot(): string { return dirname(__DIR__).'/uploads'; }
|
||||
function uploadPublicPath(string $subdir, string $filename): string {
|
||||
return '/uploads/'.trim($subdir, '/').'/'.$filename;
|
||||
}
|
||||
function cleanUploadSubdir(string $subdir): string {
|
||||
$subdir = trim(str_replace('\\', '/', $subdir), '/');
|
||||
return preg_replace('/[^a-zA-Z0-9_\/-]+/', '', $subdir) ?: 'misc';
|
||||
}
|
||||
function ensureUploadDir(string $subdir): string {
|
||||
$subdir = cleanUploadSubdir($subdir);
|
||||
$dir = uploadRoot().'/'.$subdir;
|
||||
if (!is_dir($dir)) mkdir($dir, 0775, true);
|
||||
return $dir;
|
||||
}
|
||||
function mbToBytes(mixed $mb, float $default = 5): int {
|
||||
$n = (float)$mb;
|
||||
if ($n <= 0) $n = $default;
|
||||
return max(1, (int)round($n * 1024 * 1024));
|
||||
}
|
||||
function uploadAllowedExts(string $csv, array $fallback): array {
|
||||
$raw = array_filter(array_map('trim', explode(',', strtolower($csv))));
|
||||
$exts = $raw ?: $fallback;
|
||||
return array_values(array_unique(array_map(fn($e) => ltrim($e, '.'), $exts)));
|
||||
}
|
||||
function imageExts(): array { return ['jpg','jpeg','png','gif','webp']; }
|
||||
function isImageExt(string $ext): bool { return in_array(strtolower($ext), imageExts(), true); }
|
||||
function isImagePath(string $path): bool {
|
||||
return isImageExt(strtolower(pathinfo(parse_url($path, PHP_URL_PATH) ?: $path, PATHINFO_EXTENSION)));
|
||||
}
|
||||
function uploadErrorText(int $code): string {
|
||||
return match($code) {
|
||||
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'The uploaded file is too large.',
|
||||
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially received.',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'The server is missing a temporary upload directory.',
|
||||
UPLOAD_ERR_CANT_WRITE => 'The server could not write the uploaded file.',
|
||||
UPLOAD_ERR_EXTENSION => 'A server extension stopped the upload.',
|
||||
default => 'The file could not be uploaded.',
|
||||
};
|
||||
}
|
||||
function resizeImageFile(string $path, int $thresholdBytes, int $maxWidth = 1600, int $quality = 82): bool {
|
||||
if ($thresholdBytes <= 0 || !file_exists($path) || filesize($path) <= $thresholdBytes) return false;
|
||||
if (!function_exists('getimagesize') || !function_exists('imagecreatetruecolor')) return false;
|
||||
$info = @getimagesize($path);
|
||||
if (!$info || empty($info[0]) || empty($info[1]) || empty($info['mime'])) return false;
|
||||
|
||||
[$w, $h] = $info;
|
||||
$mime = $info['mime'];
|
||||
$loader = match($mime) {
|
||||
'image/jpeg' => 'imagecreatefromjpeg',
|
||||
'image/png' => 'imagecreatefrompng',
|
||||
'image/gif' => 'imagecreatefromgif',
|
||||
'image/webp' => 'imagecreatefromwebp',
|
||||
default => '',
|
||||
};
|
||||
if ($loader === '' || !function_exists($loader)) return false;
|
||||
$src = @$loader($path);
|
||||
if (!$src) return false;
|
||||
|
||||
$scale = min(1, $maxWidth / max($w, $h));
|
||||
$nw = max(1, (int)round($w * $scale));
|
||||
$nh = max(1, (int)round($h * $scale));
|
||||
$dst = imagecreatetruecolor($nw, $nh);
|
||||
if ($mime === 'image/png' || $mime === 'image/webp') {
|
||||
imagealphablending($dst, false);
|
||||
imagesavealpha($dst, true);
|
||||
$transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127);
|
||||
imagefilledrectangle($dst, 0, 0, $nw, $nh, $transparent);
|
||||
}
|
||||
imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
|
||||
$quality = max(40, min(95, $quality));
|
||||
$ok = match($mime) {
|
||||
'image/jpeg' => imagejpeg($dst, $path, $quality),
|
||||
'image/png' => imagepng($dst, $path, 6),
|
||||
'image/gif' => imagegif($dst, $path),
|
||||
'image/webp' => function_exists('imagewebp') ? imagewebp($dst, $path, $quality) : false,
|
||||
default => false,
|
||||
};
|
||||
imagedestroy($src);
|
||||
imagedestroy($dst);
|
||||
if ($ok) @chmod($path, 0664);
|
||||
return (bool)$ok;
|
||||
}
|
||||
function normalizeUploadFiles(string $field): array {
|
||||
if (empty($_FILES[$field])) return [];
|
||||
$file = $_FILES[$field];
|
||||
if (!is_array($file['name'])) return [$file];
|
||||
$out = [];
|
||||
foreach ($file['name'] as $i => $name) {
|
||||
$out[] = [
|
||||
'name' => $name,
|
||||
'type' => $file['type'][$i] ?? '',
|
||||
'tmp_name' => $file['tmp_name'][$i] ?? '',
|
||||
'error' => $file['error'][$i] ?? UPLOAD_ERR_NO_FILE,
|
||||
'size' => $file['size'][$i] ?? 0,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
function storeUploadedFile(array $file, string $subdir, array $allowedExts, int $maxBytes, array $opts = []): array {
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return ['ok'=>true, 'path'=>''];
|
||||
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) return ['ok'=>false, 'error'=>uploadErrorText((int)$file['error'])];
|
||||
if ((int)($file['size'] ?? 0) > $maxBytes) return ['ok'=>false, 'error'=>'The uploaded file exceeds the configured size limit.'];
|
||||
|
||||
$original = (string)($file['name'] ?? 'upload');
|
||||
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
|
||||
$allowedExts = array_map('strtolower', $allowedExts);
|
||||
if ($ext === '' || !in_array($ext, $allowedExts, true)) {
|
||||
return ['ok'=>false, 'error'=>'This file type is not allowed.'];
|
||||
}
|
||||
if (!empty($opts['image_only']) && !isImageExt($ext)) return ['ok'=>false, 'error'=>'Please upload an image file.'];
|
||||
if (!empty($opts['image_only']) && function_exists('getimagesize') && !@getimagesize((string)$file['tmp_name'])) {
|
||||
return ['ok'=>false, 'error'=>'The selected file is not a valid image.'];
|
||||
}
|
||||
|
||||
$dir = ensureUploadDir($subdir);
|
||||
$filename = date('YmdHis').'-'.bin2hex(random_bytes(5)).'.'.$ext;
|
||||
$dest = $dir.'/'.$filename;
|
||||
$tmp = (string)$file['tmp_name'];
|
||||
$moved = is_uploaded_file($tmp) ? move_uploaded_file($tmp, $dest) : @rename($tmp, $dest);
|
||||
if (!$moved) return ['ok'=>false, 'error'=>'The uploaded file could not be saved.'];
|
||||
@chmod($dest, 0664);
|
||||
|
||||
if (isImageExt($ext)) {
|
||||
resizeImageFile(
|
||||
$dest,
|
||||
(int)($opts['resize_over_bytes'] ?? 0),
|
||||
(int)($opts['image_max_width'] ?? 1600),
|
||||
(int)($opts['image_quality'] ?? 82)
|
||||
);
|
||||
}
|
||||
|
||||
$mime = '';
|
||||
if (function_exists('finfo_open')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($finfo) {
|
||||
$mime = (string)finfo_file($finfo, $dest);
|
||||
finfo_close($finfo);
|
||||
}
|
||||
}
|
||||
return [
|
||||
'ok' => true,
|
||||
'path' => uploadPublicPath($subdir, $filename),
|
||||
'name' => $original,
|
||||
'mime' => $mime,
|
||||
'size' => file_exists($dest) ? filesize($dest) : (int)($file['size'] ?? 0),
|
||||
];
|
||||
}
|
||||
function storeSingleUpload(string $field, string $subdir, array $allowedExts, int $maxBytes, array $opts = []): array {
|
||||
$files = normalizeUploadFiles($field);
|
||||
return $files ? storeUploadedFile($files[0], $subdir, $allowedExts, $maxBytes, $opts) : ['ok'=>true, 'path'=>''];
|
||||
}
|
||||
function storeMultipleUploads(string $field, string $subdir, array $allowedExts, int $maxBytes, array $opts = []): array {
|
||||
$saved = [];
|
||||
foreach (normalizeUploadFiles($field) as $file) {
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) continue;
|
||||
$res = storeUploadedFile($file, $subdir, $allowedExts, $maxBytes, $opts);
|
||||
if (!$res['ok']) return ['ok'=>false, 'error'=>$res['error'] ?? 'Upload failed.', 'files'=>$saved];
|
||||
if (!empty($res['path'])) $saved[] = $res;
|
||||
}
|
||||
return ['ok'=>true, 'files'=>$saved];
|
||||
}
|
||||
function storeAvatarUpload(string $field, int $maxBytes): array {
|
||||
$files = normalizeUploadFiles($field);
|
||||
if (!$files || ($files[0]['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return ['ok'=>true, 'path'=>''];
|
||||
$file = $files[0];
|
||||
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) return ['ok'=>false, 'error'=>uploadErrorText((int)$file['error'])];
|
||||
if ((int)($file['size'] ?? 0) > $maxBytes) return ['ok'=>false, 'error'=>'The avatar exceeds the configured size limit.'];
|
||||
$ext = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
|
||||
if (!isImageExt($ext)) return ['ok'=>false, 'error'=>'Please choose a JPG, PNG, GIF, or WebP avatar.'];
|
||||
|
||||
if (!function_exists('getimagesize') || !function_exists('imagecreatetruecolor')) {
|
||||
return storeUploadedFile($file, 'avatars', imageExts(), $maxBytes, ['image_only'=>true, 'resize_over_bytes'=>mbToBytes(1), 'image_max_width'=>512, 'image_quality'=>86]);
|
||||
}
|
||||
$info = @getimagesize((string)$file['tmp_name']);
|
||||
if (!$info || empty($info[0]) || empty($info[1]) || empty($info['mime'])) return ['ok'=>false, 'error'=>'The selected avatar is not a valid image.'];
|
||||
$loader = match($info['mime']) {
|
||||
'image/jpeg' => 'imagecreatefromjpeg',
|
||||
'image/png' => 'imagecreatefrompng',
|
||||
'image/gif' => 'imagecreatefromgif',
|
||||
'image/webp' => 'imagecreatefromwebp',
|
||||
default => '',
|
||||
};
|
||||
if ($loader === '' || !function_exists($loader)) return ['ok'=>false, 'error'=>'That avatar image type is not supported by this server.'];
|
||||
$src = @$loader((string)$file['tmp_name']);
|
||||
if (!$src) return ['ok'=>false, 'error'=>'The avatar could not be opened.'];
|
||||
|
||||
$w = (int)$info[0]; $h = (int)$info[1];
|
||||
$size = (int)p('avatar_crop_size', min($w, $h));
|
||||
$x = (int)p('avatar_crop_x', max(0, (int)(($w - $size) / 2)));
|
||||
$y = (int)p('avatar_crop_y', max(0, (int)(($h - $size) / 2)));
|
||||
$size = max(1, min($size, $w, $h));
|
||||
$x = max(0, min($x, $w - $size));
|
||||
$y = max(0, min($y, $h - $size));
|
||||
|
||||
$dst = imagecreatetruecolor(512, 512);
|
||||
imagecopyresampled($dst, $src, 0, 0, $x, $y, 512, 512, $size, $size);
|
||||
$dir = ensureUploadDir('avatars');
|
||||
$filename = date('YmdHis').'-'.bin2hex(random_bytes(5)).'.jpg';
|
||||
$dest = $dir.'/'.$filename;
|
||||
$ok = imagejpeg($dst, $dest, 86);
|
||||
imagedestroy($src);
|
||||
imagedestroy($dst);
|
||||
if (!$ok) return ['ok'=>false, 'error'=>'The cropped avatar could not be saved.'];
|
||||
@chmod($dest, 0664);
|
||||
return ['ok'=>true, 'path'=>uploadPublicPath('avatars', $filename)];
|
||||
}
|
||||
function formatBytes(int $bytes): string {
|
||||
if ($bytes >= 1048576) return number_format($bytes / 1048576, 1).' MB';
|
||||
if ($bytes >= 1024) return number_format($bytes / 1024, 1).' KB';
|
||||
return $bytes.' B';
|
||||
}
|
||||
function userAvatarHtml(?array $user, string $class = 'avatar-sm'): string {
|
||||
$name = (string)($user['username'] ?? 'User');
|
||||
$path = (string)($user['avatar_path'] ?? '');
|
||||
if ($path !== '') return '<img src="'.e($path).'" alt="'.e($name).'" class="avatar '.$class.'">';
|
||||
$initial = strtoupper(substr($name, 0, 1) ?: 'U');
|
||||
return '<span class="avatar avatar-initial '.$class.'" aria-hidden="true">'.e($initial).'</span>';
|
||||
}
|
||||
function uniqueForumSlug(string $title, int $id): string {
|
||||
$base = makeSlug($title) ?: 'topic';
|
||||
return $base.'-'.$id;
|
||||
}
|
||||
function forumTopicUrl(array $topic): string {
|
||||
return '/forum/'.rawurlencode((string)$topic['slug']).'/';
|
||||
}
|
||||
|
||||
/* ── Email verification / Mailgun ─────────────────────── */
|
||||
function requestBaseUrl(): string {
|
||||
$configured = trim(setting('site_base_url', ''));
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
<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>
|
||||
<?php if ((authed() && setting('forum_enabled','1')==='1') || isAdmin()): ?>
|
||||
<li><a href="/forum" class="nl<?=($activeNav??'')==='forum'?' active':''?>">Forum</a></li>
|
||||
<?php endif; ?>
|
||||
<li><a href="/about.php" class="nl<?=($activeNav??'')==='about'?' active':''?>">About</a></li>
|
||||
<?php if (authed()): ?>
|
||||
<?php if (isAdmin()): ?>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?php
|
||||
$__requestPath = (string)(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '');
|
||||
if (preg_match('#^/forum/[^/]+/?$#', $__requestPath)) {
|
||||
require __DIR__.'/forum.php';
|
||||
exit;
|
||||
}
|
||||
require_once __DIR__.'/includes/boot.php';
|
||||
$pageTitle='Discover Keyser WV — The Friendliest City in the U.S.A.';
|
||||
$activeNav='home';
|
||||
@@ -82,6 +87,52 @@ include __DIR__.'/includes/header.php';
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="home-band">
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<div class="eyebrow">UPCOMING EVENTS</div>
|
||||
<h2 class="sec-title">What’s Happening Around Keyser</h2>
|
||||
</div>
|
||||
<a href="/events.php" class="btn btn-outline btn-sm">All Events</a>
|
||||
</div>
|
||||
<?php if($upevts): ?>
|
||||
<div class="event-strip">
|
||||
<?php foreach($upevts as $ev): ?>
|
||||
<div class="evt-card">
|
||||
<?php if(!empty($ev['image_path'])): ?>
|
||||
<div class="evt-image-wrap"><img src="<?=e($ev['image_path'])?>" alt="<?=e($ev['title'])?>" class="evt-image"></div>
|
||||
<?php endif; ?>
|
||||
<div class="evt-date-box">
|
||||
<div class="evt-mon"><?=strtoupper(date('M',strtotime($ev['event_date'])))?></div>
|
||||
<div class="evt-day"><?=date('j',strtotime($ev['event_date']))?></div>
|
||||
<div class="evt-yr"><?=date('Y',strtotime($ev['event_date']))?></div>
|
||||
</div>
|
||||
<div class="evt-body">
|
||||
<div class="evt-title"><?=e($ev['title'])?><?php if($ev['is_featured']): ?> <span class="featured-badge">FEATURED</span><?php endif; ?></div>
|
||||
<div class="evt-meta">
|
||||
<?php if($ev['venue']): ?><span><?=e($ev['venue'])?></span><?php endif; ?>
|
||||
<?php if($ev['event_time']): ?><span><?=e($ev['event_time'])?></span><?php endif; ?>
|
||||
<span><?=e($ev['cost'])?></span>
|
||||
</div>
|
||||
<div class="evt-desc"><?=e(substr($ev['description'],0,160)).(strlen($ev['description'])>160?'...':'')?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty event-empty">
|
||||
<div class="empty-ico">◇</div>
|
||||
<h3>No upcoming approved events yet</h3>
|
||||
<p style="margin-top:.5rem;color:var(--text-d)">Community events will appear here as they are approved.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="text-center mt3">
|
||||
<a href="/events.php?submit=1" class="btn btn-primary">Submit an Event</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
@@ -121,49 +172,6 @@ include __DIR__.'/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-band">
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<div class="eyebrow">UPCOMING EVENTS</div>
|
||||
<h2 class="sec-title">What’s Happening Around Keyser</h2>
|
||||
</div>
|
||||
<a href="/events.php" class="btn btn-outline btn-sm">All Events</a>
|
||||
</div>
|
||||
<?php if($upevts): ?>
|
||||
<div class="event-strip">
|
||||
<?php foreach($upevts as $ev): ?>
|
||||
<div class="evt-card">
|
||||
<div class="evt-date-box">
|
||||
<div class="evt-mon"><?=strtoupper(date('M',strtotime($ev['event_date'])))?></div>
|
||||
<div class="evt-day"><?=date('j',strtotime($ev['event_date']))?></div>
|
||||
<div class="evt-yr"><?=date('Y',strtotime($ev['event_date']))?></div>
|
||||
</div>
|
||||
<div class="evt-body">
|
||||
<div class="evt-title"><?=e($ev['title'])?><?php if($ev['is_featured']): ?> <span class="featured-badge">FEATURED</span><?php endif; ?></div>
|
||||
<div class="evt-meta">
|
||||
<?php if($ev['venue']): ?><span><?=e($ev['venue'])?></span><?php endif; ?>
|
||||
<?php if($ev['event_time']): ?><span><?=e($ev['event_time'])?></span><?php endif; ?>
|
||||
<span><?=e($ev['cost'])?></span>
|
||||
</div>
|
||||
<div class="evt-desc"><?=e(substr($ev['description'],0,160)).(strlen($ev['description'])>160?'...':'')?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty event-empty">
|
||||
<div class="empty-ico">◇</div>
|
||||
<h3>No upcoming approved events yet</h3>
|
||||
<p style="margin-top:.5rem;color:var(--text-d)">Community events will appear here as they are approved.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="text-center mt3">
|
||||
<a href="/events.php?submit=1" class="btn btn-primary">Submit an Event</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section about-split">
|
||||
<div>
|
||||
<div class="eyebrow">ABOUT KEYSER</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ requireLogin();
|
||||
$bizId=iget('id');
|
||||
$biz=qone("SELECT * FROM businesses WHERE id=?",[$bizId]);
|
||||
if(!$biz||!owns($bizId)){flash('Access denied.','error');go('/dashboard.php');}
|
||||
$menuImageLimit=max(0,min(1,(int)setting('menu_item_image_limit','1')));
|
||||
$menuImagesEnabled=$menuImageLimit>0;
|
||||
|
||||
if(isPost()){
|
||||
csrfCheck();
|
||||
@@ -28,8 +30,25 @@ if(isPost()){
|
||||
$sid=(int)p('section_id');
|
||||
$name=ps('item_name');
|
||||
if($name){
|
||||
$imagePath='';
|
||||
if($menuImagesEnabled){
|
||||
$upload=storeSingleUpload(
|
||||
'item_image',
|
||||
'menus',
|
||||
imageExts(),
|
||||
mbToBytes(setting('menu_upload_max_mb','5'),5),
|
||||
[
|
||||
'image_only'=>true,
|
||||
'resize_over_bytes'=>mbToBytes(setting('forum_image_resize_threshold_mb','1'),1),
|
||||
'image_max_width'=>(int)setting('forum_image_max_width','1600'),
|
||||
'image_quality'=>(int)setting('forum_image_quality','82'),
|
||||
]
|
||||
);
|
||||
if(!$upload['ok']){flash($upload['error'],'error');go("/menu.php?id=$bizId");}
|
||||
$imagePath=(string)($upload['path']??'');
|
||||
}
|
||||
$order=(int)qval("SELECT COALESCE(MAX(sort_order),0)+1 FROM menu_items WHERE section_id=?",[$sid]);
|
||||
qrun("INSERT INTO menu_items(section_id,name,description,price,sort_order)VALUES(?,?,?,?,?)",[$sid,$name,ps('item_desc'),(float)p('item_price',0),$order]);
|
||||
qrun("INSERT INTO menu_items(section_id,name,description,price,image_path,sort_order)VALUES(?,?,?,?,?,?)",[$sid,$name,ps('item_desc'),(float)p('item_price',0),$imagePath,$order]);
|
||||
flash('Item added!','success');
|
||||
}
|
||||
}
|
||||
@@ -39,8 +58,29 @@ if(isPost()){
|
||||
}
|
||||
if($act==='edit_item'){
|
||||
$iid=(int)p('iid');
|
||||
qrun("UPDATE menu_items SET name=?,description=?,price=?,is_available=? WHERE id=?",[ps('item_name'),ps('item_desc'),(float)p('item_price',0),(int)p('is_available',1),$iid]);
|
||||
flash('Item updated!','success');
|
||||
$item=qone("SELECT mi.* FROM menu_items mi JOIN menu_sections ms ON ms.id=mi.section_id WHERE mi.id=? AND ms.business_id=?",[$iid,$bizId]);
|
||||
if($item){
|
||||
$imagePath=(string)($item['image_path']??'');
|
||||
if($menuImagesEnabled && p('remove_item_image','0')==='1') $imagePath='';
|
||||
if($menuImagesEnabled){
|
||||
$upload=storeSingleUpload(
|
||||
'item_image',
|
||||
'menus',
|
||||
imageExts(),
|
||||
mbToBytes(setting('menu_upload_max_mb','5'),5),
|
||||
[
|
||||
'image_only'=>true,
|
||||
'resize_over_bytes'=>mbToBytes(setting('forum_image_resize_threshold_mb','1'),1),
|
||||
'image_max_width'=>(int)setting('forum_image_max_width','1600'),
|
||||
'image_quality'=>(int)setting('forum_image_quality','82'),
|
||||
]
|
||||
);
|
||||
if(!$upload['ok']){flash($upload['error'],'error');go("/menu.php?id=$bizId");}
|
||||
if(!empty($upload['path'])) $imagePath=(string)$upload['path'];
|
||||
}
|
||||
qrun("UPDATE menu_items SET name=?,description=?,price=?,image_path=?,is_available=? WHERE id=?",[ps('item_name'),ps('item_desc'),(float)p('item_price',0),$imagePath,(int)p('is_available',1),$iid]);
|
||||
flash('Item updated!','success');
|
||||
}
|
||||
}
|
||||
go("/menu.php?id=$bizId");
|
||||
}
|
||||
@@ -97,10 +137,13 @@ include __DIR__.'/includes/header.php';
|
||||
<?php foreach($items[$sec['id']] as $item): ?>
|
||||
<div style="border-bottom:1px solid var(--bdr);padding:.75rem 0">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem">
|
||||
<div>
|
||||
<div class="menu-owner-row">
|
||||
<?php if(!empty($item['image_path'])): ?><img src="<?=e($item['image_path'])?>" alt="<?=e($item['name'])?>" class="menu-owner-thumb"><?php endif; ?>
|
||||
<div>
|
||||
<div style="font-weight:600;color:<?=$item['is_available']?'var(--text)':'var(--text-d)'?>"><?=e($item['name'])?> <?php if(!$item['is_available']): ?><span class="badge badge-gray">Unavailable</span><?php endif; ?></div>
|
||||
<?php if($item['description']): ?><div style="font-size:.8rem;color:var(--text-d)"><?=e($item['description'])?></div><?php endif; ?>
|
||||
<?php if($item['price']>0): ?><div style="color:var(--gold);font-family:'Oswald',sans-serif;font-size:.9rem"><?=money((float)$item['price'])?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row" style="gap:.3rem;flex-shrink:0">
|
||||
<button onclick="this.closest('div').parentElement.querySelector('.edit-item-form').classList.toggle('hidden')" class="btn btn-xs btn-secondary">✏️</button>
|
||||
@@ -108,12 +151,25 @@ include __DIR__.'/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-item-form hidden" style="background:var(--bg4);border:1px solid var(--bdr);border-radius:4px;padding:.85rem;margin-top:.65rem">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="edit_item"><input type="hidden" name="iid" value="<?=$item['id']?>">
|
||||
<form method="POST" enctype="multipart/form-data"><?=csrfField()?><input type="hidden" name="_act" value="edit_item"><input type="hidden" name="iid" value="<?=$item['id']?>">
|
||||
<div class="form-row" style="margin-bottom:.6rem">
|
||||
<div class="fg"><label class="flabel">NAME</label><input type="text" name="item_name" class="finput" value="<?=e($item['name'])?>" required></div>
|
||||
<div class="fg"><label class="flabel">PRICE ($)</label><input type="number" name="item_price" class="finput" value="<?=e($item['price'])?>" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" value="<?=e($item['description'])?>"></div>
|
||||
<?php if($menuImagesEnabled): ?>
|
||||
<div class="fg">
|
||||
<label class="flabel">ITEM IMAGE</label>
|
||||
<?php if(!empty($item['image_path'])): ?>
|
||||
<div class="menu-admin-image">
|
||||
<img src="<?=e($item['image_path'])?>" alt="<?=e($item['name'])?>">
|
||||
<label style="display:flex;align-items:center;gap:.45rem;font-size:.82rem;color:var(--text-m);cursor:pointer"><input type="checkbox" name="remove_item_image" value="1"> Remove current image</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="item_image" class="finput" accept="image/*">
|
||||
<div class="fhint">One image allowed. Max <?=e(setting('menu_upload_max_mb','5'))?> MB.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="fg"><label class="flabel">AVAILABLE</label><select name="is_available" class="fselect"><option value="1"<?=$item['is_available']?' selected':''?>>Yes</option><option value="0"<?=!$item['is_available']?' selected':''?>>No (hidden from menu)</option></select></div>
|
||||
<button type="submit" class="btn btn-success btn-sm">Save Item</button>
|
||||
</form>
|
||||
@@ -125,12 +181,15 @@ include __DIR__.'/includes/header.php';
|
||||
<div style="margin-top:.85rem;padding-top:.85rem">
|
||||
<details><summary style="cursor:pointer;font-size:.82rem;color:var(--gold);font-family:'Oswald',sans-serif;letter-spacing:.08em">➕ Add Item to "<?=e($sec['name'])?>"</summary>
|
||||
<div style="padding:.85rem 0">
|
||||
<form method="POST"><?=csrfField()?><input type="hidden" name="_act" value="add_item"><input type="hidden" name="section_id" value="<?=$sec['id']?>">
|
||||
<form method="POST" enctype="multipart/form-data"><?=csrfField()?><input type="hidden" name="_act" value="add_item"><input type="hidden" name="section_id" value="<?=$sec['id']?>">
|
||||
<div class="form-row" style="margin-bottom:.6rem">
|
||||
<div class="fg"><label class="flabel">ITEM NAME *</label><input type="text" name="item_name" class="finput" required placeholder="e.g. Spaghetti Carbonara"></div>
|
||||
<div class="fg"><label class="flabel">PRICE ($)</label><input type="number" name="item_price" class="finput" value="0" step="0.01" min="0"></div>
|
||||
</div>
|
||||
<div class="fg"><label class="flabel">DESCRIPTION</label><input type="text" name="item_desc" class="finput" placeholder="Brief description…"></div>
|
||||
<?php if($menuImagesEnabled): ?>
|
||||
<div class="fg"><label class="flabel">ITEM IMAGE</label><input type="file" name="item_image" class="finput" accept="image/*"><div class="fhint">Optional. One image allowed, max <?=e(setting('menu_upload_max_mb','5'))?> MB.</div></div>
|
||||
<?php endif; ?>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Add Item</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user