- [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:
@@ -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'; ?>
|
||||
Reference in New Issue
Block a user