- v1.1.0 - Redesign

This commit is contained in:
Ty Clifford
2026-06-18 12:50:44 -04:00
parent f1844abad5
commit 66f93757b6
14 changed files with 2107 additions and 387 deletions
+206 -230
View File
@@ -8,286 +8,262 @@ if (setting('registration_enabled','1') !== '1') {
$pageTitle = 'Create Account — Discover Keyser WV';
$errors = [];
$businessOptions = qall("SELECT id,name,address FROM businesses WHERE is_active=1 ORDER BY name");
$tier = p('member_type', 'member') === 'business_owner' ? 'business_owner' : 'member';
if (isPost()) {
csrfCheck();
// ── Honeypot check — bots fill hidden fields, humans don't ──
// Field named "website_url" is visually hidden; any value = bot
if (ps('website_url') !== '') {
// Silently succeed (don't tell the bot it failed)
if (ps('website_url') !== '' || ps('confirm_email') !== '') {
flash('Account created! Welcome to Discover Keyser WV.','success');
go('/index.php');
}
// ── Timing check — real humans take at least 3 seconds ──
$formTime = (int)p('_form_time', 0);
if ($formTime && (time() - $formTime) < 3) {
// Also silent — pretend success
flash('Account created! Welcome to Discover Keyser WV.','success');
go('/index.php');
}
$tier = p('member_type', 'member') === 'business_owner' ? 'business_owner' : 'member';
$un = ps('username');
$em = ps('email');
$pw = ps('password');
$pw2 = ps('password2');
$tos = p('lawful_use', '');
if (strlen($un) < 3)
$errors[] = 'Username must be at least 3 characters.';
if (!preg_match('/^[a-zA-Z0-9_]+$/', $un))
$errors[] = 'Username may only contain letters, numbers, and underscores.';
if ($em && !filter_var($em, FILTER_VALIDATE_EMAIL))
$errors[] = 'Please enter a valid email address.';
if (strlen($pw) < 6)
$errors[] = 'Password must be at least 6 characters.';
if ($pw !== $pw2)
$errors[] = 'Passwords do not match.';
if ($tos !== '1')
$errors[] = 'You must agree to use this service lawfully to create an account.';
if (!$errors && qval("SELECT id FROM users WHERE username=?", [$un]))
$errors[] = 'That username is already taken. Please choose another.';
if (!$errors && $em && qval("SELECT id FROM users WHERE email=?", [$em]))
$errors[] = 'That email address is already registered. Try signing in instead.';
$claimBizId = (int)p('claim_business_id');
$contactPhone = ps('contact_phone');
$claimBiz = null;
if (strlen($un) < 3) $errors[] = 'Username must be at least 3 characters.';
if (!preg_match('/^[a-zA-Z0-9_]+$/', $un)) $errors[] = 'Username may only contain letters, numbers, and underscores.';
if ($em && !filter_var($em, FILTER_VALIDATE_EMAIL)) $errors[] = 'Please enter a valid email address.';
if (strlen($pw) < 6) $errors[] = 'Password must be at least 6 characters.';
if ($pw !== $pw2) $errors[] = 'Passwords do not match.';
if ($tos !== '1') $errors[] = 'You must agree to use this service lawfully to create an account.';
if (!$errors && qval("SELECT id FROM users WHERE username=?", [$un])) $errors[] = 'That username is already taken. Please choose another.';
if (!$errors && $em && qval("SELECT id FROM users WHERE email=?", [$em])) $errors[] = 'That email address is already registered. Try signing in instead.';
$claimEdits = [
'description' => ps('claim_description'),
'address' => ps('claim_address'),
'phone' => ps('claim_public_phone'),
'email' => ps('claim_public_email'),
'website' => ps('claim_website'),
'video_url' => ps('claim_video_url'),
];
if ($tier === 'business_owner') {
if (!$claimBizId) {
$errors[] = 'Please select the business page you want to claim.';
} else {
$claimBiz = qone("SELECT * FROM businesses WHERE id=? AND is_active=1", [$claimBizId]);
if (!$claimBiz) $errors[] = 'Please select a valid listed business.';
}
if ($contactPhone === '' || strlen(preg_replace('/\D+/', '', $contactPhone)) < 7) {
$errors[] = 'Please enter a contact phone number for verification.';
}
if ($claimEdits['email'] !== '' && !filter_var($claimEdits['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Please enter a valid public email edit.';
}
if (!validBusinessVideoUrl($claimEdits['video_url'])) {
$errors[] = 'Business videos must be a YouTube or Vimeo URL.';
}
}
if (!$errors) {
$id = qrun(
"INSERT INTO users(username,email,password)VALUES(?,?,?)",
[$un, $em ?: null, password_hash($pw, PASSWORD_BCRYPT)]
);
$submittedEdits = 0;
if ($tier === 'business_owner' && $claimBiz) {
qrun(
"INSERT INTO business_claim_requests(business_id,user_id,contact_phone)VALUES(?,?,?)",
[$claimBizId, $id, $contactPhone]
);
foreach ($claimEdits as $field => $newValue) {
if ($newValue === '') continue;
$oldValue = (string)($claimBiz[$field] ?? '');
if ($newValue === $oldValue) continue;
qrun(
"INSERT INTO business_edit_requests(business_id,user_id,field,old_value,new_value)VALUES(?,?,?,?,?)",
[$claimBizId, $id, $field, $oldValue, $newValue]
);
$submittedEdits++;
}
}
loginUser(qone("SELECT * FROM users WHERE id=?", [$id]));
flash('Welcome to Discover Keyser WV, '.e($un).'!', 'success');
if ($tier === 'business_owner' && $claimBiz) {
$msg = 'Your account is ready. Your claim for '.$claimBiz['name'].' is queued, and we will contact you at '.$contactPhone.' within a few business days for verification.';
if ($submittedEdits) $msg .= ' '.$submittedEdits.' proposed edit'.($submittedEdits !== 1 ? 's were' : ' was').' sent to admin review.';
flash($msg, 'success');
go('/dashboard.php');
}
flash('Welcome to Discover Keyser WV, '.$un.'!', 'success');
go('/index.php');
}
}
$ownerSelected = $tier === 'business_owner';
$footExtra = <<<HTML
<script>
(() => {
const radios = document.querySelectorAll('input[name="member_type"]');
const ownerFields = document.getElementById('ownerClaimFields');
const sync = () => {
const owner = document.querySelector('input[name="member_type"]:checked')?.value === 'business_owner';
ownerFields?.classList.toggle('is-open', owner);
ownerFields?.querySelectorAll('[data-owner-required]').forEach(el => {
if (owner) el.setAttribute('required', 'required');
else el.removeAttribute('required');
});
};
radios.forEach(r => r.addEventListener('change', sync));
sync();
})();
</script>
HTML;
include __DIR__.'/includes/header.php';
?>
<style>
/* Honeypot field — visually gone, still in DOM for bots */
.hp-field {
position:absolute;
left:-9999px;
top:-9999px;
opacity:0;
height:0;
width:0;
overflow:hidden;
pointer-events:none;
tabindex:-1;
}
.benefit-grid {
display:grid;
grid-template-columns:1fr 1fr;
gap:1rem;
margin-bottom:2rem;
}
@media(max-width:640px){.benefit-grid{grid-template-columns:1fr}}
.benefit-card {
background:var(--bg3);
border:1px solid var(--bdr);
border-radius:var(--r);
padding:1.4rem;
}
.benefit-card h3 {
font-family:'Oswald',sans-serif;
font-size:.88rem;
letter-spacing:.14em;
color:var(--gold);
margin-bottom:.85rem;
padding-bottom:.55rem;
border-bottom:1px solid var(--bdr);
}
.benefit-card ul {
list-style:none;
padding:0;
margin:0;
}
.benefit-card ul li {
display:flex;
gap:.55rem;
align-items:flex-start;
font-size:.86rem;
color:var(--text-m);
line-height:1.55;
margin-bottom:.55rem;
}
.benefit-card ul li:last-child{margin-bottom:0}
.benefit-card ul li .bi {flex-shrink:0;margin-top:1px}
.owner-note {
background:rgba(26,58,92,.25);
border:1px solid rgba(42,95,153,.4);
border-radius:var(--r);
padding:1.1rem 1.3rem;
font-size:.86rem;
color:var(--text-m);
line-height:1.7;
margin-bottom:2rem;
}
.owner-note strong {color:var(--gold-l)}
.tos-check {
display:flex;
align-items:flex-start;
gap:.65rem;
background:var(--bg4);
border:1px solid var(--bdr-l);
border-radius:var(--r);
padding:1rem 1.15rem;
cursor:pointer;
transition:border-color var(--t);
}
.tos-check:has(input:checked) {border-color:var(--green-l)}
.tos-check input[type="checkbox"] {
flex-shrink:0;
margin-top:3px;
width:17px;
height:17px;
accent-color:var(--green-l);
cursor:pointer;
}
.tos-check-text {
font-size:.88rem;
color:var(--text-m);
line-height:1.6;
user-select:none;
}
.tos-check-text strong {color:var(--text)}
</style>
<div style="max-width:860px;margin:3rem auto;padding:0 1.5rem">
<!-- Page header -->
<div style="text-align:center;margin-bottom:2rem">
<div class="eyebrow" style="justify-content:center;display:flex">JOIN THE COMMUNITY</div>
<h1 style="font-size:clamp(2rem,4vw,2.8rem);margin:.4rem 0">Create Your Account</h1>
<p style="color:var(--text-m);font-size:1rem;max-width:520px;margin:.6rem auto 0;line-height:1.7">
Discover Keyser WV is your community hub for local businesses, events, and information.
Takes less than a minute.
</p>
<div class="signup-shell">
<div class="signup-intro">
<div class="eyebrow">JOIN KEYSER ONLINE</div>
<h1>Create Your Account</h1>
<p>Choose a community membership or start a verified business-owner claim for a page already listed in the directory.</p>
</div>
<!-- Benefits -->
<div class="benefit-grid">
<div class="benefit-card">
<h3>👤 COMMUNITY MEMBER BENEFITS</h3>
<ul>
<li><span class="bi">⭐</span>Rate and review local Keyser businesses</li>
<li><span class="bi">📅</span>Submit community events to the public calendar</li>
<li><span class="bi">🚩</span>Report incorrect or outdated business listings</li>
<li><span class="bi">💬</span>Share your experiences with the Keyser community</li>
<li><span class="bi">📍</span>Access your personal dashboard with your submissions</li>
<li><span class="bi">🔔</span>Stay connected with what's happening in Mineral County</li>
</ul>
<?php if ($errors): ?>
<div class="err-box signup-errors"><?=implode('<br>', array_map('e', $errors))?></div>
<?php endif; ?>
<form method="POST" autocomplete="off" class="signup-card">
<?=csrfField()?>
<input type="hidden" name="_form_time" value="<?=time()?>">
<div class="hp-field" aria-hidden="true">
<label for="website_url">Leave this blank</label>
<input type="text" id="website_url" name="website_url" value="" tabindex="-1" autocomplete="off">
<label for="confirm_email">Do not fill</label>
<input type="email" id="confirm_email" name="confirm_email" value="" tabindex="-1" autocomplete="off">
</div>
<div class="benefit-card" style="border-color:rgba(26,72,118,.5)">
<h3>🏢 BUSINESS OWNER BENEFITS</h3>
<ul>
<li><span class="bi">✏️</span>Update your listing — description, hours, phone, address</li>
<li><span class="bi">📢</span>Post alerts and announcements directly on your business page</li>
<li><span class="bi">🍽️</span>Build and manage a full menu for your restaurant or café</li>
<li><span class="bi">🟢</span>Mark your business open or temporarily closed in real time</li>
<li><span class="bi">📊</span>View your listing's reviews and ratings from one dashboard</li>
<li><span class="bi">🔑</span>All changes are reviewed by our admin team before going live</li>
</ul>
<div class="tier-grid">
<label class="tier-card">
<input type="radio" name="member_type" value="member"<?=!$ownerSelected?' checked':''?>>
<span class="tier-kicker">Regular Member</span>
<span class="tier-title">Explore and contribute</span>
<span class="tier-copy">Rate businesses, submit events, report outdated listings, and keep up with Keyser.</span>
</label>
<label class="tier-card tier-card-owner">
<input type="radio" name="member_type" value="business_owner"<?=$ownerSelected?' checked':''?>>
<span class="tier-kicker">Business Owner</span>
<span class="tier-title">Claim a listed page</span>
<span class="tier-copy">Request ownership, send initial corrections, and add an optional YouTube or Vimeo video.</span>
</label>
</div>
</div>
<!-- Business owner request note -->
<div class="owner-note">
<strong>🏢 Do you own or manage a Keyser business?</strong> Create your account below,
then <strong>contact our admin team</strong> at
<a href="mailto:<?=e(setting('contact_email','info@discoverkeyser.com'))?>" style="color:var(--gold-l)"><?=e(setting('contact_email','info@discoverkeyser.com'))?></a>
with your username and the name of your business.
An administrator will link your account to your listing so you can start managing it.
This verification step helps us ensure only legitimate owners control business pages.
</div>
<!-- Registration form -->
<div style="background:var(--bg3);border:1px solid var(--bdr);border-radius:var(--r-lg);padding:2.25rem">
<h2 style="font-family:'Oswald',sans-serif;font-size:1rem;letter-spacing:.14em;color:var(--text);margin-bottom:1.5rem;padding-bottom:.65rem;border-bottom:1px solid var(--bdr)">
CREATE YOUR ACCOUNT
</h2>
<?php if ($errors): ?>
<div class="err-box" style="margin-bottom:1.25rem"><?=implode('<br>', array_map('e', $errors))?></div>
<?php endif; ?>
<form method="POST" autocomplete="off">
<?=csrfField()?>
<!-- Timing token -->
<input type="hidden" name="_form_time" value="<?=time()?>">
<!-- ── HONEYPOT fields — hidden from humans, filled by bots ── -->
<div class="hp-field" aria-hidden="true">
<label for="website_url">Leave this blank</label>
<input type="text" id="website_url" name="website_url" value=""
tabindex="-1" autocomplete="off">
<div class="signup-grid">
<div class="fg">
<label class="flabel" for="username">USERNAME *</label>
<input type="text" id="username" name="username" class="finput"
value="<?=e($_POST['username']??'')?>"
autocomplete="username" required autofocus pattern="[a-zA-Z0-9_]+" minlength="3">
<div class="fhint">Letters, numbers, underscores only. Min 3 characters.</div>
</div>
<div class="hp-field" aria-hidden="true">
<label for="confirm_email">Do not fill</label>
<input type="email" id="confirm_email" name="confirm_email" value=""
tabindex="-1" autocomplete="off">
<div class="fg">
<label class="flabel" for="email">EMAIL ADDRESS <span style="font-weight:300;letter-spacing:0">(optional)</span></label>
<input type="email" id="email" name="email" class="finput"
value="<?=e($_POST['email']??'')?>"
autocomplete="email">
<div class="fhint">Used for account recovery and owner verification follow-up.</div>
</div>
<!-- ── END HONEYPOT ─────────────────────────────────────────── -->
<div class="fg">
<label class="flabel" for="password">PASSWORD *</label>
<input type="password" id="password" name="password" class="finput" autocomplete="new-password" required minlength="6">
</div>
<div class="fg">
<label class="flabel" for="password2">CONFIRM PASSWORD *</label>
<input type="password" id="password2" name="password2" class="finput" autocomplete="new-password" required minlength="6">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
<div class="fg" style="margin-bottom:0">
<label class="flabel" for="username">USERNAME *</label>
<input type="text" id="username" name="username" class="finput"
value="<?=e($_POST['username']??'')?>"
autocomplete="username" required autofocus
pattern="[a-zA-Z0-9_]+" minlength="3">
<div class="fhint">Letters, numbers, underscores only. Min 3 characters.</div>
<div id="ownerClaimFields" class="owner-claim-fields<?=$ownerSelected?' is-open':''?>">
<div class="owner-claim-note">
<strong>Verification required.</strong> Select the business page you manage and provide a contact phone number. A site administrator will contact you within a few business days before the page is assigned to your account.
</div>
<div class="signup-grid">
<div class="fg">
<label class="flabel" for="claim_business_id">BUSINESS TO CLAIM *</label>
<select id="claim_business_id" name="claim_business_id" class="fselect" data-owner-required>
<option value="">Select a listed business</option>
<?php foreach ($businessOptions as $biz): ?>
<option value="<?=$biz['id']?>"<?=((int)($_POST['claim_business_id']??0)===(int)$biz['id'])?' selected':''?>>
<?=e($biz['name'])?><?= $biz['address'] ? ' — '.e($biz['address']) : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="fg" style="margin-bottom:0">
<label class="flabel" for="email">EMAIL ADDRESS <span style="font-weight:300;letter-spacing:0">(optional)</span></label>
<input type="email" id="email" name="email" class="finput"
value="<?=e($_POST['email']??'')?>"
autocomplete="email">
<div class="fhint">Used only for account recovery. Never shared.</div>
<div class="fg">
<label class="flabel" for="contact_phone">CONTACT PHONE FOR VERIFICATION *</label>
<input type="tel" id="contact_phone" name="contact_phone" class="finput"
value="<?=e($_POST['contact_phone']??'')?>" placeholder="304-555-0123" data-owner-required>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:1rem">
<div class="fg" style="margin-bottom:0">
<label class="flabel" for="password">PASSWORD *</label>
<input type="password" id="password" name="password" class="finput"
autocomplete="new-password" required minlength="6">
<div class="fhint">Minimum 6 characters.</div>
</div>
<div class="fg" style="margin-bottom:0">
<label class="flabel" for="password2">CONFIRM PASSWORD *</label>
<input type="password" id="password2" name="password2" class="finput"
autocomplete="new-password" required minlength="6">
<div class="claim-edit-panel">
<div class="ibox-title">OPTIONAL FIRST EDITS</div>
<div class="signup-grid">
<div class="fg signup-wide">
<label class="flabel" for="claim_description">DESCRIPTION UPDATE</label>
<textarea id="claim_description" name="claim_description" class="ftextarea" placeholder="Share a corrected or refreshed business description."><?=e($_POST['claim_description']??'')?></textarea>
</div>
<div class="fg">
<label class="flabel" for="claim_address">PUBLIC ADDRESS</label>
<input type="text" id="claim_address" name="claim_address" class="finput" value="<?=e($_POST['claim_address']??'')?>">
</div>
<div class="fg">
<label class="flabel" for="claim_public_phone">PUBLIC PHONE</label>
<input type="text" id="claim_public_phone" name="claim_public_phone" class="finput" value="<?=e($_POST['claim_public_phone']??'')?>">
</div>
<div class="fg">
<label class="flabel" for="claim_public_email">PUBLIC EMAIL</label>
<input type="email" id="claim_public_email" name="claim_public_email" class="finput" value="<?=e($_POST['claim_public_email']??'')?>">
</div>
<div class="fg">
<label class="flabel" for="claim_website">WEBSITE</label>
<input type="text" id="claim_website" name="claim_website" class="finput" value="<?=e($_POST['claim_website']??'')?>" placeholder="example.com">
</div>
<div class="fg signup-wide">
<label class="flabel" for="claim_video_url">VIDEO URL</label>
<input type="url" id="claim_video_url" name="claim_video_url" class="finput" value="<?=e($_POST['claim_video_url']??'')?>" placeholder="YouTube or Vimeo URL">
<div class="fhint">Optional. Business owners may use YouTube or Vimeo.</div>
</div>
</div>
</div>
</div>
<!-- Terms / lawful use checkbox -->
<div style="margin:1.5rem 0 1.25rem">
<label class="tos-check">
<input type="checkbox" name="lawful_use" value="1"
<?=(!empty($_POST) && p('lawful_use')==='1') ? 'checked' : ''?> required>
<span class="tos-check-text">
<strong>I will only use this service lawfully.</strong>
I agree not to post false, misleading, defamatory, or harmful content.
I understand that accounts used for spam, abuse, or fraudulent business claims
will be removed without notice.
</span>
</label>
</div>
<button type="submit" class="btn btn-primary btn-block" style="font-size:.9rem;padding:.85rem">
Create Account
</button>
<p style="text-align:center;margin-top:1.1rem;font-size:.84rem;color:var(--text-d)">
Already have an account? <a href="/login.php">Sign in here</a>
</p>
</form>
</div>
<label class="tos-check">
<input type="checkbox" name="lawful_use" value="1" <?=(!empty($_POST) && p('lawful_use')==='1') ? 'checked' : ''?> required>
<span class="tos-check-text">
<strong>I will only use this service lawfully.</strong>
I agree not to post false, misleading, defamatory, or harmful content, and I understand fraudulent business claims may be removed.
</span>
</label>
<button type="submit" class="btn btn-primary btn-block signup-submit">Create Account</button>
<p class="signin-link">Already have an account? <a href="/login.php">Sign in here</a></p>
</form>
</div>
<?php include __DIR__.'/includes/footer.php'; ?>