PageSourceSearch

https://exquisite-kitten-9ab8bf.netlify.app/account

html exquisite-kitten-9ab8bf.netlify.app collected 2026-10-03 10:43:03 UTC 17,547 bytes, 322 lines download raw bytes

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.