1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1"> 6<title>My Account â Needham Market FC</title> 7<meta name="robots" content="noindex, follow"> 8<link rel="icon" type="image/png" href="assets/nmfc-crest.png"> 9<link rel="stylesheet" href="/fonts/css"> 10<style> 11:root { --red:#C8102E; --red-deep:#8B0A1F; --red-soft:#E73B53; --red-tint:#FDE9EC; --bg:#F4EFE3; --bg-2:#EAE3D0; --surface:#FBF8F1; --paper:#FFF; --ink:#1B1B22; --ink-soft:#5A5A62; --mute:#8C8A82; --line:rgba(27,27,34,0.10); --line-strong:rgba(27,27,34,0.18); --gold:#B68A3A; } 12* { margin:0; padding:0; box-sizing:border-box; } 13body { background:var(--bg); color:var(--ink); font-family:'DM Sans',system-ui,sans-serif; font-size:15px; line-height:1.55; } 14a { color:inherit; text-decoration:none; } 15.wrap { max-width:760px; margin:0 auto; padding:0 24px; } 16header.nav { background:var(--surface); border-bottom:1px solid var(--line); padding:16px 0; } 17.nav-inner { display:flex; align-items:center; gap:14px; max-width:1140px; margin:0 auto; padding:0 24px; } 18.nav-inner img { width:44px; height:44px; } 19.brand-text { font-family:'Anton',sans-serif; font-size:16px; letter-spacing:0.04em; text-transform:uppercase; line-height:1.1; } 20.brand-text small { display:block; font-family:'DM Sans',sans-serif; font-size:10px; letter-spacing:0.18em; color:var(--mute); text-transform:uppercase; } 21.nav-links { margin-left:auto; display:flex; gap:22px; font-size:12px; letter-spacing:0.12em; text-transform:uppercase; font-weight:600; } 22.nav-links a:hover { color:var(--red); } 23h1 { font-family:'Anton',sans-serif; font-size:clamp(40px,6vw,64px); line-height:0.95; text-transform:uppercase; margin:48px 0 8px; } 24h1 .it { font-family:'Fraunces',serif; font-style:italic; font-weight:400; text-transform:lowercase; color:var(--red); } 25.sub { color:var(--ink-soft); margin-bottom:36px; } 26.card { background:var(--surface); border:1px solid var(--line); padding:30px 28px; margin-bottom:18px; } 27.card h3 { font-family:'Anton',sans-serif; font-size:20px; letter-spacing:0.03em; text-transform:uppercase; margin-bottom:18px; } 28label { display:block; font-size:11px; letter-spacing:0.18em; text-transform:uppercase; font-weight:600; margin:14px 0 5px; } 29input { width:100%; padding:11px 13px; border:1px solid var(--line-strong); background:var(--paper); font-family:inherit; font-size:14.5px; } 30input:focus { outline:2px solid var(--red); outline-offset:-1px; } 31.btn { display:inline-block; margin-top:20px; padding:13px 28px; background:var(--red); color:#fff; border:none; cursor:pointer; font-family:inherit; font-size:12px; font-weight:700; letter-spacing:0.18em; text-transform:uppercase; } 32.btn:hover { background:var(--red-deep); } 33.btn.ghost { background:var(--ink); } 34.btn[disabled] { opacity:0.5; cursor:wait; } 35.toggle { margin-top:18px; font-size:13px; color:var(--ink-soft); } 36.toggle a { color:var(--red); font-weight:700; cursor:pointer; } 37.msg { margin-top:16px; padding:12px 16px; font-size:13.5px; display:none; } 38.msg.show { display:block; } 39.msg.ok { background:#EAF5EC; border:1px solid #BCDFC3; color:#1E5C2A; } 40.msg.err { background:var(--red-tint); border:1px solid rgba(200,16,46,0.3); color:var(--red-deep); } 41.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:16px; } 42@media (max-width:600px){ .grid2 { grid-template-columns:1fr; } } 43.bk { border:1px solid var(--line); background:var(--paper); padding:16px 18px; margin-bottom:10px; display:flex; gap:16px; align-items:center; flex-wrap:wrap; } 44.bk .fac { font-size:11px; letter-spacing:0.16em; text-transform:uppercase; color:var(--red); font-weight:700; } 45.bk .when { font-family:'Anton',sans-serif; font-size:18px; } 46.bk .meta { font-size:12.5px; color:var(--ink-soft); } 47.bk .status { margin-left:auto; font-size:10px; letter-spacing:0.16em; text-transform:uppercase; font-weight:700; padding:5px 10px; } 48.status.pending { background:#FCF3DC; color:#8A6515; } 49.status.confirmed { background:#EAF5EC; color:#1E5C2A; } 50.status.declined, .status.cancelled { background:var(--bg-2); color:var(--mute); } 51.bk .cancel { font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:var(--mute); border:1px solid var(--line); padding:5px 10px; background:none; cursor:pointer; font-family:inherit; } 52.bk .cancel:hover { color:var(--red); border-color:var(--red); } 53.empty { padding:34px; text-align:center; color:var(--mute); font-family:'Fraunces',serif; font-style:italic; border:1px dashed var(--line-strong); } 54footer { margin-top:80px; padding:30px 0; border-top:1px solid var(--line); font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:var(--mute); text-align:center; } 55</style> 56</head> 57<body> 58 59<header class="nav"> 60 <div class="nav-inner"> 61 <a href='/' style='display:flex; align-items:center; gap:12px;'> 62 <img src="assets/nmfc-crest.png" alt="Needham Market FC"> 63 <div class="brand-text">Needham Market FC<small>My Account</small></div> 64 </a> 65 <nav class="nav-links"> 66 <a href='/'>Home</a> 67 <a href='/bookings'>Book Venue</a> 68 <a id="nav-signout" href="#" style="display:none; color:var(--red);">Sign Out</a> 69 </nav> 70 </div> 71</header> 72 73<div class="wrap"> 74 75 <!-- ============ signed OUT ============ --> 76 <div id="auth-view" style="display:none;"> 77 <h1>Your<br><span class="it">account.</span></h1> 78 <p class="sub">Create a free account to request bookings at Bloomfields and track them in one place. We'll verify your email â no passwords shared with anyone, no payments online.</p> 79 80 <div class="card" id="signup-card"> 81 <h3>Create Account</h3> 82 <form id="su-form"> 83 <div class="grid2"> 84 <div><label>Full Name</label><input type="text" id="su-name" autocomplete="name" placeholder="e.g. James Palmer" required></div> 85 <div><label>Phone (optional)</label><input type="tel" id="su-phone" autocomplete="tel" placeholder="07xxx xxxxxx"></div> 86 </div> 87 <label>Email</label><input type="email" id="su-email" autocomplete="email" placeholder="[email protected]" required> 88 <label>Password</label><input type="password" id="su-pass" autocomplete="new-password" placeholder="8+ characters" minlength="8" required> 89 <button class="btn" id="su-btn" type="submit">Create Account â</button> 90 </form> 91 <div class="msg" id="su-msg"></div> 92 <div class="toggle">Already have an account? <a id="show-signin">Sign in</a></div> 93 </div> 94 95 <div class="card" id="signin-card" style="display:none;"> 96 <h3>Sign In</h3> 97 <form id="si-form"> 98 <label>Email</label><input type="email" id="si-email" autocomplete="email" required> 99 <label>Password</label><input type="password" id="si-pass" autocomplete="current-password"> 100 <div style="display:flex; gap:10px; flex-wrap:wrap;"> 101 <button class="btn" id="si-btn" type="submit">Sign In â</button> 102 <button class="btn ghost" id="si-reset" type="button">
102Forgot Password</button> 103 </div> 104 </form> 105 <div class="msg" id="si-msg"></div> 106 <div class="toggle">New here? <a id="show-signup">Create an account</a></div> 107 </div> 108 </div> 109 110 <!-- ============ set new password (arrived via reset email) ============ --> 111 <div id="reset-view" style="display:none;"> 112 <h1>New<br><span class="it">password.</span></h1> 113 <p class="sub">You've followed a password reset link â choose a new password below. You'll be signed in straight away.</p> 114 <div class="card"> 115 <h3>Set New Password</h3> 116 <form id="rp-form"> 117 <label>New Password</label><input type="password" id="rp-pass" autocomplete="new-password" placeholder="8+ characters" minlength="8" required> 118 <label>Confirm New Password</label><input type="password" id="rp-pass2" autocomplete="new-password" required> 119 <button class="btn" id="rp-btn" type="submit">Save New Password â</button> 120 </form> 121 <div class="msg" id="rp-msg"></div> 122 </div> 123 </div> 124 125 <!-- ============ signed IN ============ --> 126 <div id="account-view" style="display:none;"> 127 <h1>Hello,<br><span class="it" id="hello-name">there.</span></h1> 128 <p class="sub" id="acct-email"></p> 129 130 <div class="card" id="admin-card" style="display:none; border-left:4px solid var(--red);"> 131 <h3>Admin</h3> 132 <p style="color:var(--ink-soft); margin-bottom:4px;" id="admin-pending-line">Checking pending requestsâ¦</p> 133 <a class='btn' href='/admin-bookings'>Open Booking Dashboard â</a> 134 </div> 135 136 <div class="card"> 137 <h3>My Bookings</h3> 138 <div id="my-bookings"><div class="empty">Loadingâ¦</div></div> 139 <a class='btn' href='/bookings'>Request a New Booking â</a> 140 </div> 141 142 <div class="card"> 143 <h3>My Details</h3> 144 <div class="grid2"> 145 <div><label>Full Name</label><input type="text" id="pf-name"></div> 146 <div><label>Phone</label><input type="tel" id="pf-phone"></div> 147 </div> 148 <label>Group / Organisation</label><input type="text" id="pf-org" placeholder="Optional"> 149 <button class="btn" id="pf-save">Save Details</button> 150 <div class="msg" id="pf-msg"></div> 151 </div> 152 </div> 153 154</div> 155 156<footer>© 2026 Needham Market FC · Bloomfields · One Club, One Community</footer> 157
158<script src="https://cdn.jsdelivr.net/npm/@supabase/[email protected]/dist/umd/supabase.min.js" integrity="sha384-0UK+HVlz5Y7F//atDpPysyocv/PjGXQoBX+XSaL/eEotARW8rPFh+lL5sO0Ljzfi" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
vendor: 1 bytes, line 158
158
159<script src="booking-config.js"></script>
vendor: 1 bytes, line 159
159
160<script> 161const sb = window.supabase.createClient(window.NMFC_SUPABASE_URL, window.NMFC_SUPABASE_ANON_KEY); 162const FAC = window.NMFC_FACILITIES; 163const $ = (id) => document.getElementById(id); 164 165function msg(id, text, ok) { const m = $(id); m.textContent = text; m.className = 'msg show ' + (ok ? 'ok' : 'err'); } 166 167$('show-signin').onclick = () => { $('signup-card').style.display = 'none'; $('signin-card').style.display = 'block'; }; 168$('show-signup').onclick = () => { $('signin-card').style.display = 'none'; $('signup-card').style.display = 'block'; }; 169 170function friendlyAuthError(m) { 171 if (/invalid login credentials/i.test(m)) return 'Wrong email or password. Forgotten it? Use the Forgot Password button.'; 172 if (/email not confirmed/i.test(m)) return 'Please verify your email first â check your inbox for the link.'; 173 if (/already registered/i.test(m)) return 'That email already has an account â sign in instead, or use Forgot Password.'; 174 if (/rate limit/i.test(m)) return 'Too many attempts â wait a minute and try again.'; 175 return m; 176} 177 178$('su-form').addEventListener('submit', async (e) => { 179 e.preventDefault(); 180 const btn = $('su-btn'); btn.disabled = true; 181 const { error } = await sb.auth.signUp({ 182 email: $('su-email').value.trim(), 183 password: $('su-pass').value, 184 options: { data: { full_name: $('su-name').value.trim(), phone: $('su-phone').value.trim() }, 185 emailRedirectTo: location.origin + '/account.html' } 186 }); 187 btn.disabled = false; 188 if (error) return msg('su-msg', friendlyAuthError(error.message), false); 189 msg('su-msg', 'Almost there â check your inbox and click the verification link to activate your account.', true); 190}); 191 192$('si-form').addEventListener('submit', async (e) => { 193 e.preventDefault(); 194 const btn = $('si-btn'); btn.disabled = true; 195 const { error } = await sb.auth.signInWithPassword({ email: $('si-email').value.trim(), password: $('si-pass').value }); 196 btn.disabled = false; 197 if (error) return msg('si-msg', friendlyAuthError(error.message), false); 198 render(); 199}); 200 201$('si-reset').onclick = async () => { 202 const email = $('si-email').value.trim(); 203 if (!email) return msg('si-msg', 'Enter your email first, then press Forgot Password.', false); 204 const { error } = await sb.auth.resetPasswordForEmail(email, { redirectTo: location.origin + '/account.html' }); 205 msg('si-msg', error ? friendlyAuthError(error.message) : 'Password reset email sent â open the link in your inbox to set a new password.', !error); 206}; 207 208// ---- password recovery: user arrived via the reset email link ---- 209let recovering = false; 210$('rp-form').addEventListener('submit', async (e) => { 211 e.preventDefault(); 212 if ($('rp-pass').value !== $('rp-pass2').value) return msg('rp-msg', 'Passwords don\'t match.', false); 213 const btn = $('rp-btn'); btn.disabled = true; 214 const { error } = await sb.auth.updateUser({ password: $('rp-pass').value }); 215 btn.disabled = false; 216 if (error) return msg('rp-msg', friendlyAuthError(error.message), false); 217 msg('rp-msg', 'Password updated â you\'re signed in.', true); 218 recovering = false; 219 setTimeout(render, 900); 220}); 221 222// surface errors from expired/invalid email links (Supabase puts them in the URL hash) 223(function () { 224 const h = new URLSearchParams(location.hash.replace(/^#/, '')); 225 if (h.get('error_description')) { 226 $('signup-card').style.display = 'none'; $('signin-card').style.display = 'block'; 227 msg('si-msg', h.get('error_description').replace(/\+/g, ' ') + ' â request a new link with Forgot Password.', false); 228 history.replaceState(null, '', location.pathname); 229 } 230})(); 231 232$('nav-signout').onclick = async (e) => { e.preventDefault(); try { localStorage.removeItem('nmfc-session-hint'); } catch (_) {} await sb.auth.signOut(); render(); }; 233 234async function loadProfile(user) { 235 const { data } = await sb.from('profiles').select('*').eq('id', user.id).single(); 236 if (data) { 237 $('hello-name').textContent = (data.full_name || 'there').split(' ')[0] + '.'; 238 $('pf-name').value = data.full_name || ''; 239 $('pf-phone').value = data.phone || ''; 240 $('pf-org').value = data.organisation || ''; 241 // hint for lightweight nav state on other pages 242 try { localStorage.setItem('nmfc-session-hint', JSON.stringify({ email: user.email, role: data.role })); } catch (e) {} 243 if (data.role === 'admin') { 244 $('admin-card').style.display = 'block'; 245 const { count } = await sb.from('bookings').select('id', { count: 'exact', head: true }).eq('status', 'pending'); 246 $('admin-pending-line').textContent = count === 0 ? 'No pending booking requests. â' 247 : count + ' booking request' + (count === 1 ? '' : 's') + ' awaiting a decision.'; 248 } else { 249 $('admin-card').style.display = 'none'; 250 } 251 } 252} 253 254$('pf-save').onclick = async () => { 255 const { data: { user } } = await sb.auth.getUser(); 256 const { error } = await sb.from('profiles').update({ 257 full_name: $('pf-name').value.trim(), phone: $('pf-phone').value.trim(), organisation: $('pf-org').value.trim() 258 }).eq('id', user.id); 259 msg('pf-msg', error ? error.message : 'Saved.', !error); 260}; 261 262async function loadBookings(user) { 263 const { data, error } = await sb.from('bookings').select('*') 264 .eq('user_id', user.id).neq('status', 'cancelled') 265 .gte('date', new Date().toISOString().slice(0, 10)) 266 .order('date').order('start_hour'); 267 const box = $('my-bookings'); 268 if (error) { box.innerHTML = '<div class="empty">Could not load bookings.</div>'; return; } 269 if (!data.length) { box.innerHTML = '<div class="empty">No upcoming bookings â request one below.</div>'; return; } 270 box.innerHTML = ''; 271 data.forEach(b => {
272 const row = document.createElement('div'); row.className = 'bk'; 273 const d = new Date(b.date + 'T00:00:00'); 274 const left = document.createElement('div'); 275 left.innerHTML = '<div class="fac"></div><div class="when"></div><div class="meta"></div>'; 276 left.querySelector('.fac').textContent = (FAC[b.facility] || { name: b.facility }).name; 277 left.querySelector('.when').textContent = d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }); 278 left.querySelector('.meta').textContent = String(b.start_hour).padStart(2, '0') + ':00 â ' + String(b.start_hour + b.duration).padStart(2, '0') + ':00 · ' + (b.purpose || 'No notes'); 279 const st = document.createElement('span'); st.className = 'status ' + b.status; st.textContent = b.status; 280 row.appendChild(left); row.appendChild(st); 281 if (b.status === 'pending' || b.status === 'confirmed') { 282 const c = document.createElement('button'); c.className = 'cancel'; c.textContent = 'Cancel'; 283 c.onclick = async () => { 284 if (!confirm('Cancel this booking?')) return; 285 await sb.from('bookings').update({ status: 'cancelled' }).eq('id', b.id); 286 loadBookings(user); 287 }; 288 row.appendChild(c); 289 } 290 box.appendChild(row); 291 }); 292} 293 294async function render() { 295 if (recovering) { 296 $('auth-view').style.display = 'none'; 297 $('account-view').style.display = 'none'; 298 $('reset-view').style.display = 'block'; 299 $('nav-signout').style.display = 'none'; 300 return; 301 } 302 $('reset-view').style.display = 'none'; 303 const { data: { session } } = await sb.auth.getSession(); 304 $('auth-view').style.display = session ? 'none' : 'block'; 305 $('account-view').style.display = session ? 'block' : 'none'; 306 $('nav-signout').style.display = session ? 'inline' : 'none'; 307 if (session) { 308 $('acct-email').textContent = 'Signed in as ' + session.user.email; 309 loadProfile(session.user); loadBookings(session.user); 310 } 311} 312// Defer renders out of the auth callback â queries inside it can deadlock (supabase-js lock). 313sb.auth.onAuthStateChange((event) => { 314 if (event === 'PASSWORD_RECOVERY') { recovering = true; } 315 setTimeout(render, 0); 316}); 317render(); 318</script>
318 319
320<script defer src="magic-link.js"></script>
320<script defer src="nav.js"></script>
320 321</body> 322 </html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.