Files
MyKeyser/account.php
T
Ty Clifford 25327e3302 - [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.
2026-06-19 10:49:48 -04:00

138 lines
6.8 KiB
PHP

<?php
require_once __DIR__.'/includes/boot.php';
requireLogin();
$pageTitle = 'Account Settings — Keyser WV';
$u = qone("SELECT * FROM users WHERE id=?",[uid()]);
$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.';
if ($pw !== $pw2) $errors[] = 'Passwords do not match.';
}
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 (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';
?>
<div class="form-box" style="max-width:520px">
<h1 class="form-title">Account Settings</h1>
<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" 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>
<div class="fg"><label class="flabel">NEW PASSWORD</label><input type="password" name="password" class="finput" autocomplete="new-password"></div>
<div class="fg"><label class="flabel">CONFIRM NEW PASSWORD</label><input type="password" name="password2" class="finput" autocomplete="new-password"></div>
<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'; ?>