// Federation Resources hub — document library + meetings/workshops calendar (public, federation-facing).
const CTSCHub = (function () {
const DOC_CATS = ['Constitution', 'Meeting Notice', 'Agenda', 'Minutes', 'Correspondence', 'Policy'];
const DOC_META = {
'Constitution': { ic: '§', c: 'var(--navy)' },
'Meeting Notice': { ic: '◷', c: 'var(--gold-d)' },
'Agenda': { ic: '☷', c: 'var(--ocean)' },
'Minutes': { ic: '✎', c: 'var(--ok)' },
'Correspondence': { ic: '✉', c: 'var(--warn)' },
'Policy': { ic: '◆', c: 'var(--navy2)' },
};
const TYPE_META = {
'Meeting': { c: 'var(--navy)', label: 'Meeting' },
'Workshop': { c: 'var(--ocean)', label: 'Workshop' },
'AGM': { c: 'var(--gold)', label: 'AGM' },
'Deadline': { c: 'var(--warn)', label: 'Deadline' },
'Event': { c: 'var(--ok)', label: 'Event' },
};
const CAL_TYPES = ['Meeting', 'Workshop', 'AGM', 'Deadline', 'Event'];
const extColor = (ext) => ({ PDF: '#b21f1f', DOCX: '#1453a6', DOC: '#1453a6', XLSX: '#1f8a5b', XLS: '#1f8a5b' }[(ext || '').toUpperCase()] || 'var(--muted)');
const WD = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const ymd = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
// ---- Reusable month calendar grid ----
function MonthCalendar({ events, month, onPrev, onNext, selected, onSelect }) {
const first = new Date(month.getFullYear(), month.getMonth(), 1);
const lead = (first.getDay() + 6) % 7; // Mon=0
const daysIn = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
const todayStr = ymd(new Date());
const byDay = {};
events.forEach(e => { (byDay[e.date] = byDay[e.date] || []).push(e); });
const cells = [];
for (let i = 0; i < lead; i++) cells.push(null);
for (let d = 1; d <= daysIn; d++) cells.push(d);
while (cells.length % 7 !== 0) cells.push(null);
return (
‹
{MONTHS[month.getMonth()]} {month.getFullYear()}
›
{cells.map((d, i) => {
if (d === null) return
;
const ds = ymd(new Date(month.getFullYear(), month.getMonth(), d));
const evs = byDay[ds] || [];
const isToday = ds === todayStr;
const isSel = ds === selected;
return (
onSelect && onSelect(isSel ? null : ds)} style={{ minHeight: 92, textAlign: 'left', padding: '7px 8px', verticalAlign: 'top',
borderRight: (i % 7 !== 6) ? '1px solid var(--line2)' : 'none', borderBottom: '1px solid var(--line2)',
background: isSel ? 'var(--sky-tint)' : '#fff', cursor: evs.length || onSelect ? 'pointer' : 'default', position: 'relative' }}>
{d}
{evs.slice(0, 2).map(e => (
{e.title}
))}
{evs.length > 2 &&
+{evs.length - 2} more
}
);
})}
);
}
return { DOC_CATS, DOC_META, TYPE_META, CAL_TYPES, extColor, MonthCalendar, MONTHS, ymd };
})();
window.CTSCHub = CTSCHub;
// ---- Public Resources page ----
function ResourcesPage() {
const { data } = CTSCStore.useStore();
const { fmtDate, fedName } = CTSCStore;
const { DOC_CATS, DOC_META, TYPE_META, MonthCalendar, MONTHS } = CTSCHub;
const [view, setView] = React.useState('documents');
// documents
const [q, setQ] = React.useState('');
const [cat, setCat] = React.useState('All');
const docs = (data.documents || []).filter(d => d.status !== 'Draft');
const filtered = docs.filter(d => {
if (cat !== 'All' && d.category !== cat) return false;
if (q.trim() && !((d.title + ' ' + (d.note || '')).toLowerCase().includes(q.toLowerCase().trim()))) return false;
return true;
}).sort((a, b) => (a.date < b.date ? 1 : -1));
// calendar
const today = new Date();
const [month, setMonth] = React.useState(new Date(today.getFullYear(), today.getMonth(), 1));
const [sel, setSel] = React.useState(null);
const cal = (data.calendar || []).slice().sort((a, b) => (a.date + (a.time || '')).localeCompare(b.date + (b.time || '')));
const upcoming = cal.filter(e => e.date >= CTSCHub.ymd(today)).slice(0, 6);
const selEvents = sel ? cal.filter(e => e.date === sel) : null;
return (
{/* hero */}
Federation Resources
Documents & Calendar
Official documents the Council shares with affiliated federations — constitutions, meeting notices, agendas, minutes and correspondence — plus the calendar of meetings and workshops.
{[['documents', '☷ Documents'], ['calendar', '◷ Calendar']].map(([id, label]) => (
setView(id)} style={{ padding: '13px 22px', fontSize: 14.5, fontWeight: 800, color: view === id ? 'var(--navy)' : '#cfe1ec',
background: view === id ? 'var(--sand)' : 'transparent', borderRadius: '8px 8px 0 0',
borderTop: '1px solid ' + (view === id ? 'transparent' : 'rgba(255,255,255,.18)'),
borderLeft: '1px solid ' + (view === id ? 'transparent' : 'rgba(255,255,255,.18)'),
borderRight: '1px solid ' + (view === id ? 'transparent' : 'rgba(255,255,255,.18)'),
borderBottom: 'none' }}>{label}
))}
{view === 'documents' ? (
{/* toolbar */}
{['All', ...DOC_CATS].map(c => setCat(c)}>{c} )}
{/* list */}
{filtered.length === 0 ? (
☷
No documents match your filters
) : (
{filtered.map(d => {
const meta = DOC_META[d.category] || { ic: '◆', c: 'var(--navy)' };
return (
{meta.ic}
{(d.ext || 'PDF').toUpperCase()}
{d.title}
{d.category}
{d.note &&
{d.note}
}
{d.fedId ? fedName(data, d.fedId) : 'All federations'} · {fmtDate(d.date)} · {d.size || '—'}
↓ Download
);
})}
)}
) : (
setMonth(m => new Date(m.getFullYear(), m.getMonth() - 1, 1))}
onNext={() => setMonth(m => new Date(m.getFullYear(), m.getMonth() + 1, 1))}
selected={sel} onSelect={setSel} />
{Object.entries(TYPE_META).map(([k, v]) => (
{v.label}
))}
{sel ? CTSCStore.fmtDate(sel) : 'Upcoming'}
{(selEvents || upcoming).length === 0 ? (
{sel ? 'Nothing scheduled this day.' : 'No upcoming items.'}
) : (
{(selEvents || upcoming).map(e => (
{e.title}
{!sel && fmtDate(e.date)}{!sel && (e.time ? ' · ' : '')}{e.time && (e.time + (e.end ? '–' + e.end : ''))}
{e.venue &&
⚲ {e.venue}
}
{TYPE_META[e.type].label}
))}
)}
{sel &&
setSel(null)}>Show all upcoming }
)}
);
}
window.ResourcesPage = ResourcesPage;