diff --git a/.gitignore b/.gitignore index 48d831a..9023c17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ data/*.db data/*.db-shm data/*.db-wal +uploads/* +!uploads/.gitkeep diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..8ef5afe --- /dev/null +++ b/.htaccess @@ -0,0 +1,3 @@ +RewriteEngine On +RewriteRule ^forum/?$ forum.php [L,QSA] +RewriteRule ^forum/([^/]+)/?$ forum.php?topic=$1 [L,QSA] diff --git a/account.php b/account.php index 97b5f19..265696f 100644 --- a/account.php +++ b/account.php @@ -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';

Manage your Discover Keyser WV account

ACCOUNT INFO
+
πŸ‘€ ADMIN':''?>
πŸ“…Joined
πŸ•Last login:
',array_map('e',$errors))?>
-
+ +
+ +
+ +
+ +
+ + + +
+ + + +
Optional JPG, PNG, GIF, or WebP. Max MB.
+ + + +
+
+

CHANGE PASSWORD (leave blank to keep current)

@@ -42,4 +72,66 @@ include __DIR__.'/includes/header.php';
+ +(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(); +})(); + +HTML; ?> diff --git a/admin/admin_header.php b/admin/admin_header.php index 437539c..a382137 100644 --- a/admin/admin_header.php +++ b/admin/admin_header.php @@ -25,6 +25,7 @@ $_pendingClaims = (int)qval("SELECT COUNT(*) FROM business_claim_requests WHERE
COMMUNITY
+ πŸ’¬ Forum πŸ“… Events πŸ—ΊοΈ Attractions 🚩 Reports diff --git a/admin/events.php b/admin/events.php index 6508d5a..07f2384 100644 --- a/admin/events.php +++ b/admin/events.php @@ -1,6 +1,7 @@ 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'; ?>
-
+
@@ -60,6 +80,19 @@ $events = qall("SELECT e.*,u.username FROM events e LEFT JOIN users u ON u.id=e.
+
+ + +
+ <?=e($editing['title'] ?? 'Event image')?> + +
+ + +
Optional JPG, PNG, GIF, or WebP. Max MB.
+
@@ -90,7 +123,7 @@ $events = qall("SELECT e.*,u.username FROM events e LEFT JOIN users u ON u.id=e. - + Image':''?> 'badge-green','pending'=>'badge-gold','rejected'=>'badge-red']; ?>"> diff --git a/admin/forum.php b/admin/forum.php new file mode 100644 index 0000000..06cb0d6 --- /dev/null +++ b/admin/forum.php @@ -0,0 +1,223 @@ + 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 +"); +?> + + +
+$value): ?> +
+ +
+ +
+
+
FORUM SETTINGS
+ +
+ +
+ + +
+
+
+
+
Comma-separated extensions.
+
+
+
+
+
+
+ + +
+ +
+
ADD CATEGORY
+
+
+
+
+
+
+
+ +
+
+
+ +
CATEGORIES
+
+ + + + + + + + + + + + + + +
NameDescriptionTopicsStatusOrderActions
Active':'Inactive'?> +
+ Edit +
+ +
+ + +
+ + + +
+
+
+
+
+ +
RECENT TOPICS
+
+ + + + + + + + + + + + + + + +
TopicCategoryByRepliesStatusActions
+ Pinned + Locked + +
+
+
+
+
+
No forum topics yet.
+
+ + diff --git a/admin/index.php b/admin/index.php index 2c207de..a99f467 100644 --- a/admin/index.php +++ b/admin/index.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 +"); ?> @@ -156,6 +166,28 @@ $pendingEdits = qall("
+ +
+
FORUM ACTIVITY
+
+ + + + + + + + + + + + + +
TOPICCATEGORYBYREPLIES
Moderate
+
+
+ +
🏒 RECENT BUSINESSES
diff --git a/admin/menus.php b/admin/menus.php index 7bea50c..5da8898 100644 --- a/admin/menus.php +++ b/admin/menus.php @@ -1,9 +1,11 @@ 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'; ?> @@ -117,11 +156,12 @@ if ($biz) {
- + + @@ -132,12 +172,25 @@ if ($biz) {
ITEM NAMEDESCRIPTIONPRICEAVAIL.ORDER
ITEM NAMEIMAGEDESCRIPTIONPRICEAVAIL.ORDER
<?=e($item['name'])?>β€” 0?money($item['price']):'β€”'?> Yes':'No'?>