/* global React, NA_DATA, NASite, naSupabase */
const { useState, useEffect } = React;
// ============== TEAM ENQUIRY MODAL ==============
function TeamEnquiryModal({ squad, onClose }) {
const [status, setStatus] = useState('idle'); // 'idle' | 'sending' | 'sent' | 'error'
const [errMsg, setErrMsg] = useState('');
const [form, setForm] = useState({ first_name: '', last_name: '', email: '', player_info: '', message: '' });
const [hp, setHp] = useState(''); // honeypot — must stay empty
const startedAt = React.useRef(Date.now());
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
if (!squad) return null;
const submit = async (e) => {
e.preventDefault();
setStatus('sending');
setErrMsg('');
// Spam guard — silently succeed without writing or starting a mandate.
if (naSupabase.looksLikeSpam({ honeypot: hp, startedAt: startedAt.current })) {
setStatus('sent');
return;
}
try {
// Save the trial enquiry to Supabase so the coach app sees it.
// No payment / Direct Debit at this stage — this is just an enquiry;
// the coach follows up and sets up payment separately if it goes ahead.
await naSupabase.submitEnquiry({
source: 'team_trial',
squad_id: squad.id,
first_name: form.first_name.trim(),
last_name: form.last_name.trim(),
email: form.email.trim(),
player_info: form.player_info.trim(),
message: form.message.trim(),
});
setStatus('sent');
} catch (err) {
console.warn('Trial enquiry failed:', err);
setErrMsg(err.message || 'Could not send. Try again or email admin@nottinghamathletic.com.');
setStatus('error');
}
};
const sent = status === 'sent';
const sending = status === 'sending';
return (
e.stopPropagation()}>
{sent ? (
Message sent.
{squad.coach} will be in touch within a few days about trying out for {squad.name}.
Close
) : (
<>
Enquire · {squad.name}
Message {squad.coach}.
Trials, late entry, or just a question — drop a line and the head coach will come back to you.
>
)}
);
}
// ============== TEAMS PAGE ==============
function TeamsPage({ setPage }) {
const { Photo, FilterBar } = NASite;
const { SQUADS, COACHES } = NA_DATA;
const [active, setActive] = useState('all');
const [enquireSquad, setEnquireSquad] = useState(null);
const [rosters, setRosters] = useState({}); // { squad_id: [players] }
const [fixturesBySquad, setFixturesBySquad] = useState({});
const [rosterStatus, setRosterStatus] = useState('loading'); // 'loading' | 'ok' | 'error' | 'empty'
// Pull rosters + upcoming fixtures from Supabase on mount.
useEffect(() => {
let cancelled = false;
(async () => {
try {
const [roster, fixtures] = await Promise.all([
naSupabase.fetchRosters(),
naSupabase.fetchFixtures(),
]);
if (cancelled) return;
setRosters(naSupabase.groupBy(roster, 'squad_id'));
const upcoming = {};
fixtures
.map(naSupabase.normaliseFixture)
.filter(f => f.status === 'upcoming')
.forEach(f => { if (!upcoming[f.squad]) upcoming[f.squad] = f; });
setFixturesBySquad(upcoming);
setRosterStatus(roster.length ? 'ok' : 'empty');
} catch (e) {
if (!cancelled) setRosterStatus('error');
console.warn('Supabase rosters/fixtures fetch failed:', e);
}
})();
return () => { cancelled = true; };
}, []);
const TIERS = [
{ id: 'national', title: 'National League', sub: 'Invite only — players are selected at trials.' },
{ id: 'ybl', title: 'YBL', sub: 'Registration open — prices coming soon.' },
{ id: 'local', title: 'Local League', sub: 'Open to all · sign up online and pay monthly by Direct Debit.' },
];
// U12 (tier 'open') is shown alongside the Local League teams.
const groupFor = (id) => (id === 'open' ? 'local' : id);
const levelColor = (lvl) => {
if (lvl === 'National') return { bg: '#FCD53A', fg: '#0A1626' };
if (lvl === 'Regional') return { bg: '#1E4FA8', fg: '#fff' };
if (lvl === 'University') return { bg: '#2d65c9', fg: '#fff' };
return { bg: '#e5ebfa', fg: '#0A1626' };
};
return (
Club / Teams
The squads.
Eleven teams across three tiers — National League (invite only), YBL, and our open Local League. Local League and U12 are open to all; click a team to sign up or message the coach.
{TIERS.map(tier => {
const squadsInTier = SQUADS.filter(s => groupFor(s.tier) === tier.id);
if (!squadsInTier.length) return null;
return (
{squadsInTier.map(s => {
const coach = COACHES.find(c => c.squad === s.id);
const upcoming = fixturesBySquad[s.id];
const roster = rosters[s.id] || [];
return (
{s.tag}
{s.name}
{s.blurb}
{/* Leagues — multi-entry */}
Competing in
{s.leagues.map((lg, i) => {
const c = levelColor(lg.level);
return (
{lg.level}
{lg.name}
{lg.record}
{lg.position}
);
})}
{/* Roster — only published for adult squads (Senior, U23).
Junior rosters stay private for safeguarding. */}
{roster.length > 0 && ['senior','u23'].includes(s.id) && (
Roster · {roster.length}
Live from coach app
{roster.map((p, i) => (
{p.jersey_no != null && #{p.jersey_no} }
{p.first_name} {p.last_initial}.
))}
)}
Coach
{coach ? coach.name : s.coach}
Monthly
{s.inviteOnly ? 'Invite only' : s.priceTBC ? 'Coming soon' : `£${s.monthly}/mo`}
{s.bookable ? (
setPage('sessions')}>
Sign up & pay · £{s.monthly}/mo →
) : (
setEnquireSquad(s)}>
{s.inviteOnly ? `Enquire about ${s.short} →` : `Register interest →`}
)}
Next fixture
{upcoming ? (
<>
{upcoming.fullDate.replace(/^[A-Za-z]+ /, '')}
{upcoming.fullDate.split(' ')[0]}
{upcoming.home ? 'vs' : 'at'} {' '}
{upcoming.opponent}
Tip-off {upcoming.tipoff}
Venue {upcoming.venue || 'TBC'}
>
) : (
<>
TBC
Next fixture to be confirmed.
>
)}
);
})}
);
})}
setEnquireSquad(null)} />
);
}
// ============== FIXTURES PAGE ==============
// Reads from Supabase view `fixtures`. Falls back to in-code FIXTURES if empty
// or unreachable. No code editing — fixtures are added via the coach app.
function FixturesPage() {
const { FixtureRow, FilterBar } = NASite;
const { FIXTURES, SQUADS } = NA_DATA;
const [tab, setTab] = useState('upcoming');
const [squad, setSquad] = useState('all');
const [expanded, setExpanded] = useState(null);
const [remoteFixtures, setRemoteFixtures] = useState(null);
const [loadState, setLoadState] = useState('loading'); // 'loading' | 'ok' | 'empty' | 'error'
const [loadMsg, setLoadMsg] = useState('');
useEffect(() => {
let cancelled = false;
(async () => {
try {
const rows = await naSupabase.fetchFixtures();
if (cancelled) return;
const parsed = rows.map(naSupabase.normaliseFixture);
setRemoteFixtures(parsed);
if (parsed.length === 0) {
setLoadState('empty');
} else {
setLoadState('ok');
setLoadMsg(`${parsed.length} fixture${parsed.length === 1 ? '' : 's'} live from the coach app.`);
}
} catch (e) {
if (cancelled) return;
setLoadState('error');
setLoadMsg(e.message || 'Could not reach Supabase');
}
})();
return () => { cancelled = true; };
}, []);
// Only ever render REAL fixtures from the coach app. Never fall back to
// placeholder games — an unpublished schedule shows "to be announced".
const sourceFixtures = remoteFixtures || [];
const tabs = [
{ id: 'upcoming', label: 'Upcoming' },
{ id: 'past', label: 'Results' },
];
const squadFilters = [
{ id: 'all', label: 'All' },
...SQUADS.map(s => ({ id: s.id, label: s.short, dotClass: s.dotClass })),
];
const filtered = sourceFixtures.filter(f =>
f.status === tab && (squad === 'all' || f.squad === squad)
);
return (
Club / Fixtures
Fixtures & results.
Every match across the club, published by the coaches. Filter by squad. Tap any row for venue, tip-off, and travel notes.
{/* Live-data banner */}
{loadState === 'ok' ? '✓' : loadState === 'loading' ? '…' : loadState === 'error' ? '!' : '✎'}
{loadState === 'ok' && 'Live from the coach app'}
{loadState === 'loading' && 'Loading fixtures…'}
{loadState === 'error' && 'Couldn’t reach the coach app'}
{loadState === 'empty' && 'Schedule to be announced'}
{loadState === 'ok' && ` — ${loadMsg} Add or edit fixtures in the coach app and they appear here automatically.`}
{loadState === 'loading' && ' — fetching live data from Supabase.'}
{loadState === 'error' && ` — ${loadMsg}. Check back shortly.`}
{loadState === 'empty' && ' — fixtures will appear here as soon as the coaches publish them.'}
{filtered.length === 0 ? (
{loadState === 'loading'
? 'Loading…'
: tab === 'upcoming'
? (squad === 'all'
? 'Schedule to be announced — no fixtures published yet. Check back soon.'
: 'Schedule to be announced for this squad. Check back soon.')
: 'No results to show yet.'}
) : (
{filtered.map(fx => (
setExpanded(expanded === fx.id ? null : fx.id)} />
))}
)}
);
}
window.TeamsPage = TeamsPage;
window.FixturesPage = FixturesPage;