Internal Development Reference · Not Public-Facing
Access at /internal/reference. Not linked in the public navigation.
| Name | File Path / Value | Notes |
|---|---|---|
| Home | pages/Home.jsx/ | Block-driven. Renders the PageBlock records of the 'home' Page in sort_order: a hero block, an announcement_marquee block, and three builtin_section blocks (News & Updates, This Week's Highlights, Sponsor Strip) whose JSX lives in this file as local components. Hero is matched by block_type, not list position. The featured events strip fetches all Events and sorts them with compareEventsByDateThenStart before taking the first four, so a later-starting event can never be picked over an earlier one on the same date. StripeBar is always on and not admin-managed. Falls back to the original hardcoded layout if the block query fails or the Page record is missing. Defines NewsUpdatesGrid. Reads Event, NewsArticle, Page, PageBlock. |
| About | pages/DynamicPage.jsx/about | There is no About.jsx. The route renders <DynamicPage pageKey="about" />, which loads the Page record with page_key 'about' and renders its PageBlock records. Editable in Site Pages. |
| Sponsorship | pages/DynamicPage.jsx/sponsorship | There is no Sponsorship.jsx. Renders <DynamicPage pageKey="sponsorship" />. Editable in Site Pages. |
| Dynamic Page (by key) | pages/DynamicPage.jsx(via pageKey prop) | Generic CMS page renderer. Takes a pageKey prop, loads the matching Page plus its PageBlock records, and renders them through BlockRenderer. Used by the two system pages above. |
| Dynamic Page (by slug) | pages/DynamicPageBySlug.jsx/pages/:slug | Generic route for admin-created CMS pages. Resolves the slug via useParams and delegates rendering. Any page created in Site Pages that is not a system page lives here. |
| Events | pages/Events.jsx/events | Full schedule. Reads the Event entity; day filter bar with EventTicketCard grids. |
| Event Details | pages/EventDetails.jsx/events/:id | Single event view. Reads Event and FeaturedArtist by route id. Shows 'Event not found.' when the id does not resolve. |
| Alumni | pages/Alumni.jsx/alumni | Alumni engagement page with a registration form that writes to the AlumniSubmission entity. |
| News | pages/News.jsx/news | News index. Reads NewsArticle and NewsCategory with category filtering. |
| News Article Detail | pages/NewsArticleDetail.jsx/news/:slug | Single article view, resolved by slug from the NewsArticle entity. Article content is ReactQuill HTML and is rendered through dangerouslySetInnerHTML, so it MUST pass through sanitizeRichText first. Never render article.content raw. |
| Why Partner | pages/WhyPartner.jsx/why-partner | Sponsorship rationale page. Renders the FAQ accordion from the FaqCategory and Faq entities, managed in FAQ Admin. |
| Activation Opportunities | pages/ActivationOpportunities.jsx/activation-opportunities | Static sponsor activation content. No entity reads. |
| A La Carte | pages/AlaCarte.jsx/ala-carte | Static a-la-carte sponsorship options. No entity reads. |
| Travel Details | pages/TravelDetails.jsx/travel-details | Static travel and lodging information, including estimated rates. No entity reads. |
| EndZone Experience | pages/EndZoneExperience.jsx/endzone-experience | Static EndZone Experience promotional page. No entity reads. |
| Name | File Path / Value | Notes |
|---|---|---|
| Admin Dashboard | pages/AdminDashboard.jsx/admin | Landing page for the admin area. Counts records from Event, AlumniSubmission and SponsorshipSubmission. |
| Events Admin | pages/AdminEvents.jsx/admin/events | CRUD for the Event entity plus nested FeaturedArtist records. Uploads images via Core.UploadFile. |
| Form Submissions | pages/AdminSubmissions.jsx/admin/submissions | Reads and edits submissions, and triggers the exportSubmissions backend function for CSV download. |
| News Admin | pages/admin/NewsAdmin.jsx/admin/news | Manages NewsArticle and NewsCategory records and the featured-article flag. Also writes User.application_role, so this page grants the news role. Gated by useHasPermission('manage_news'). |
| News Article Form | pages/admin/NewsArticleForm.jsx/admin/news/new · /admin/news/edit/:id | Create and edit form for NewsArticle. One component serving both routes, distinguished by useParams. Gated by useHasPermission('manage_news'). |
| Page Settings | pages/admin/PageSettings.jsx/admin/pages | Per-page PageSettings records: live / Coming Soon / Maintenance mode, coming-soon image and text, and the in-content sidebar toggle and heading. |
| Site Pages | pages/admin/SitePages.jsx/admin/site-pages | Lists Page records and creates new ones from a starter template, seeding PageBlock records from utils/pageTemplates.js. Deleting a page also deletes its blocks. System pages cannot be deleted. |
| Site Page Editor | pages/admin/SitePageEditor.jsx/admin/site-pages/edit/:id | The Page Builder. Edits page metadata and the PageBlock list: add, edit, show/hide, reorder by sort_order swap, delete. The Edit button only renders for block types present in BLOCK_EDITOR_REGISTRY, so builtin_section blocks correctly show no editor. On the Home page it also surfaces the 'Restore built-in sections' panel. |
| Navigation | pages/admin/NavigationAdmin.jsx/admin/navigation | Manages the NavItem entity for both the header and footer Quick Links. Add, edit, delete, show/hide, one level of sub-items, move up/down reordering, and an Unlinked Pages panel listing published Pages no menu reaches. |
| FAQ Admin | pages/admin/FaqAdmin.jsx/admin/faqs | Two-tab CRUD for FaqCategory and Faq, with display ordering. Feeds the Why Partner FAQ section. |
| Name | File Path / Value | Notes |
|---|---|---|
| App Reference Guide | pages/AppReferenceGuide.jsx/internal/reference | This document. Outside the Layout and AdminLayout wrappers, so it has no navbar or admin chrome, and is not linked from any menu. Maintained by hand. |
| Page Not Found | lib/PageNotFound.jsx* | Platform-provided 404 fallback, wired to the catch-all route in App.jsx. |
| Name | File Path / Value | Notes |
|---|---|---|
| Layout | components/Layout.jsx | Wraps every public page via React Router Outlet. Renders Navbar and Footer and reads PageSettings to pass page mode down. |
| AdminLayout | components/AdminLayout.jsx | Wraps every /admin route. Sidebar link list (Dashboard, Events, Form Submissions, Page Settings, Site Pages, Navigation, FAQs), login redirect, and an 'Access Denied' state. News links are conditional on useHasPermission('manage_news'). This is where admin-area access is gated. |
| Navbar | components/Navbar.jsx | Sticky maroon #860038 header. Links come from the NavItem entity (menu='header'), not hardcoded. One level of dropdowns: opens on click, and on hover only where matchMedia('(hover: hover) and (min-width: 768px)') matches. Escape closes and restores focus to the toggle; outside click and route change also dismiss. A parent that is both a link and a dropdown renders a Link plus a separate chevron button. Mobile drawer uses an accordion with a 56x48px toggle. Falls back to DEFAULT_HEADER_ITEMS if the query returns nothing or fails. |
| Footer | components/Footer.jsx | Dark #111827 footer. Quick Links column is driven by NavItem (menu='footer') with indented sub-links and falls back to DEFAULT_FOOTER_ITEMS. Internal links use router Link. The two Homecoming 2026 promo links are intentionally hardcoded editorial content. The copyright line has a double-click-to-/admin shortcut. |
| StripeBar | components/StripeBar.jsx | Decorative 6px repeating gold/maroon gradient, aria-hidden. Contains no text and is not admin-editable. |
| ComingSoonGate | components/ComingSoonGate.jsx | Wraps a page and swaps it for a Coming Soon or Maintenance screen based on that page's PageSettings.mode. Home is wrapped in it. |
| ContentWithSidebar | components/ContentWithSidebar.jsx | Two-column shell that conditionally shows the in-content sidebar, driven by PageSettings.show_sidebar via useOutletContext. |
| SidebarNav | components/SidebarNav.jsx | In-content sidebar links for a single page, from the SidebarLink entity filtered by page_key, with its heading from PageSettings.sidebar_heading. Unrelated to the NavItem site navigation. |
| EventCard | components/EventCard.jsx | Compact event card used in the Home highlights grid. |
| EventTicketCard | components/EventTicketCard.jsx | Ticket-style event card used on the Events page. Props: dayAbbr, dateNum, month, eventName, time, location, badge, badgeVariant, flagship. Left stub is #860038, or #111827 when flagship. |
| ProtectedRoute | components/ProtectedRoute.jsx | Auth guard component using useAuth. Present in the codebase; not currently referenced by any route in App.jsx. |
| UserNotRegisteredError | components/UserNotRegisteredError.jsx | Access Restricted screen shown by App.jsx when auth returns user_not_registered. |
| shadcn/ui primitives | components/ui/ (49 files) | Standard shadcn/ui component library. Used selectively; most admin screens are styled with inline styles rather than these. |
| Name | File Path / Value | Notes |
|---|---|---|
| BlockRenderer | components/blocks/BlockRenderer.jsx | Maps a PageBlock record to its renderer via REGISTRY, parses content, and returns null for hidden blocks or unknown types. builtin_section is deliberately absent from REGISTRY because Home.jsx renders those itself. Injects page_id and page_key into inquiry_form content. |
| blockDefaults | components/blocks/blockDefaults.js | Shared FONT and BRAND tokens, the shared field style, and safeParseContent, which tolerates both JSON strings and objects and returns {} on malformed content. Also owns the hero size ceiling: HERO_MAX_RATIO_W/H (2033x774), HERO_MAX_RATIO, HERO_MAX_HEIGHT_PX, HERO_MAX_RATIO_LABEL, plus parseAspectRatio (accepts '1920/400', '16:9' or a bare number) and clampHeroAspectRatio, which returns { ratio, css, clamped }. Both HeroBlock and HeroBlockEditor read the ceiling from here, so change it in one place only. |
| videoSource | components/blocks/videoSource.js | Parses an admin-pasted video URL into something embeddable. parseVideoSource returns { kind: 'youtube' | 'vimeo', id, embedUrl } or { kind: 'file', src }, or null. Handles watch?v=, youtu.be, /embed/, /shorts/, /live/ and Vimeo; anything unrecognised falls through to 'file' so Razuna and CDN links with signed query strings still work, with the hero poster as the fallback if the load fails. YouTube goes through youtube-nocookie.com, muted, looped and chromeless. describeVideoSource supplies the detection hint shown in the editor. |
| HeroBlock | components/blocks/HeroBlock.jsx | Hero banner in three modes: solid colour, background image, or background video. Video mode needs content.media_type === 'video' plus a background_video_url that parses; background_image_url doubles as the poster. The banner box is hard-clamped to 2033x774 in BOTH media modes — never proportionally taller than that ratio, never more than 774px tall — so no source file can enlarge the hero. Media is cropped to fill: <video> uses object-fit cover, while an iframe (which ignores object-fit) is sized by hand from the nominal ratio and centred. Sizing from the nominal ratio is deliberate: the 774px cap only ever makes the real box shorter, so the computed size can over-crop but never letterbox. Includes a 44x44 pause/play control (WCAG 2.2 SC 2.2.2) that unmounts the player, which is what actually stops a YouTube iframe, and honours prefers-reduced-motion on first render so those visitors never see a frame of video. |
| AnnouncementMarqueeBlock | components/blocks/AnnouncementMarqueeBlock.jsx | Scrolling announcement band. Admin controls text and scroll speed; maroon ground and gold text are brand-locked. The repeat count and the translateX percentage both derive from one REPEATS constant so the seamless loop cannot break. Pauses on hover and keyboard focus, and does not animate under prefers-reduced-motion. |
| TextBlock | components/blocks/TextBlock.jsx | Heading plus body copy section. |
| TextWithVisualBlock | components/blocks/TextWithVisualBlock.jsx | Split layout: copy beside an image or a large-text visual, with an optional reverse toggle. |
| MilestoneTimelineBlock | components/blocks/MilestoneTimelineBlock.jsx | Ordered milestone timeline from a list in content. |
| LeadershipMessageBlock | components/blocks/LeadershipMessageBlock.jsx | Letter-style message with a signature name and title. |
| TierGridBlock | components/blocks/TierGridBlock.jsx | Sponsorship tier or package grid. |
| CardGridBlock | components/blocks/CardGridBlock.jsx | Generic card grid. |
| CTABannerBlock | components/blocks/CTABannerBlock.jsx | Call-to-action banner with heading and buttons. |
| InquiryFormBlock | components/blocks/InquiryFormBlock.jsx | Public inquiry form. Writes to SponsorshipSubmission or to the generic PageFormSubmission entity depending on configuration. |
| Name | File Path / Value | Notes |
|---|---|---|
| BlockEditorRegistry | components/admin/pageBuilder/BlockEditorRegistry.jsx | Single source of truth for BLOCK_TYPE_LABELS, ADDABLE_BLOCK_TYPES (labels minus builtin_section), BLOCK_EDITOR_REGISTRY (editor + defaultContent per type) and summarizeBlock. A type absent from BLOCK_EDITOR_REGISTRY renders no Edit button. |
| BlockEditorDrawer | components/admin/pageBuilder/BlockEditorDrawer.jsx | Slide-over shell hosting the per-type editor with save and cancel. |
| fieldEditors | components/admin/pageBuilder/fieldEditors.jsx | Shared form controls: TextField, TextAreaField, ColorField, CheckboxField, SelectField, ImageUploadField (Core.UploadFile), StringListField, AddButton, RemoveButton, SectionDivider. |
| Block editors | components/admin/pageBuilder/blockEditors/ (10 files) | One editor per editable block type: Hero, AnnouncementMarquee, Text, TextWithVisual, MilestoneTimeline, LeadershipMessage, TierGrid, CardGrid, CTABanner, InquiryForm. Each exports its component plus a *_DEFAULT_CONTENT object. builtin_section has no editor by design. HeroBlockEditor is the only one that imports from components/blocks/: it reads the hero ceiling from blockDefaults and the URL detection hint from videoSource, so the warning it shows always matches what the renderer actually enforces. |
| Name | File Path / Value | Notes |
|---|---|---|
| Page | entities/Page.jsonc | CMS pages. Fields: page_key, title, template enum(about|home|sponsorship), status enum(draft|published), is_system_page, hero_heading, hero_subheading. RLS: public read, admin write. System pages keep fixed URLs; others live at /pages/:slug. 'home' is in the template enum only so the pre-existing Home record validates; it is not offered at creation. |
| PageBlock | entities/PageBlock.jsonc | Ordered content blocks belonging to a Page. Fields: page_id, block_type enum(hero|text|text_with_visual|milestone_timeline|leadership_message|tier_grid|card_grid|cta_banner|inquiry_form|announcement_marquee|builtin_section), content (JSON string), sort_order, is_visible. RLS: public read, admin write. |
| NavItem | entities/NavItem.jsonc | Header and footer navigation. Fields: menu enum(header|footer), label, link_type enum(page|route|external|anchor|none), page_key, url, parent_id, sort_order, is_active, open_in_new_tab. RLS: public read, admin write. Only one level of nesting renders. Distinct from SidebarLink. |
| SidebarLink | entities/SidebarLink.jsonc | In-content sidebar links for one page. Fields: page_key, label, url, is_external, is_active, sort_order. RLS: public read, admin write. Not site navigation. |
| PageSettings | entities/PageSettings.jsonc | Per-page display settings. Fields: page_key, mode, coming_soon_image_url, coming_soon_text, sidebar_heading, show_sidebar. RLS: public read, admin write. |
| Event | entities/Event.jsonc | Fields: name, description, date, start_time, end_time, location, ticket_link, sort_order, image_url, category enum(Concert|Cultural|Athletic|Alumni|Worship Experience). RLS: public read (anonymous visitors read this on Events, Home and Event Details), admin create/update/delete. |
| FeaturedArtist | entities/FeaturedArtist.jsonc | Performers attached to an Event. Fields: event_id, artist_name, artist_image_url, performance_time, sort_order. RLS: public read (Event Details reads it anonymously), admin create/update/delete. |
| NewsArticle | entities/NewsArticle.jsonc | Fields: title, slug, content, excerpt, featured_image_url, published_date, status enum(draft|published), author_name, category, is_featured. RLS: public read, admin write. |
| NewsCategory | entities/NewsCategory.jsonc | Fields: name, sort_order, is_active. RLS: public read, admin write. |
| FaqCategory | entities/FaqCategory.jsonc | Fields: name, display_order, is_active. RLS: public read, admin write. |
| Faq | entities/Faq.jsonc | Fields: question, answer, category_id, display_order, is_active. RLS: public read, admin write. |
| SponsorshipSubmission | entities/SponsorshipSubmission.jsonc | Sponsorship enquiries. Fields: org_name, contact_name, email, phone, tier, message, status enum(new|contacted|confirmed|declined). RLS: public create (the public inquiry form submits anonymously), admin read/update/delete. Contains contact PII, so read must stay admin-only. |
| AlumniSubmission | entities/AlumniSubmission.jsonc | Alumni registrations. Fields: first_name, last_name, grad_year, degree, city, state, email, phone, updates, status enum(new|reviewed|archived). RLS: public create (the Alumni page form submits anonymously), admin read/update/delete. Contains contact PII, so read must stay admin-only. |
| PageFormSubmission | entities/PageFormSubmission.jsonc | Generic submissions from inquiry_form blocks. Fields: page_id, page_key, name, email, phone, message, extra_fields, status enum(new|reviewed|archived). RLS: public create, admin read/update/delete. read was public until 30 Jul 2026, which exposed every submission; do not set it back to true. |
| RolePermission | entities/RolePermission.jsonc | Fields: role_name, permission_key, is_granted. RLS: public read, admin write. Read by PermissionsContext. Only permission_key currently in use is manage_news. |
| User | entities/User.jsonc | Built-in user entity, extended with role enum(admin|user) and application_role. Written by News Admin when granting roles. Declares no rls block: permissions are platform-managed and the Security Scan does not flag it. Left deliberately alone. |
| Name | File Path / Value | Notes |
|---|---|---|
| exportSubmissions | base44/functions/exportSubmissions/entry.ts | Deno function returning a CSV of SponsorshipSubmission or AlumniSubmission records. Checks user.role === 'admin' and returns 403 before using asServiceRole, and whitelists the entity_name argument. Per the Base44 changelog of 22 Jun 2026, asServiceRole bypasses entity access rules entirely, so that in-function admin check is the ONLY thing protecting this data. Do not remove it. Invoked directly via base44.functions.invoke, so it has no function.jsonc and is not automation-triggered, which is correct for an HTTP-invoked function. |
| Name | File Path / Value | Notes |
|---|---|---|
| Navigation helpers | utils/navigation.js | buildNavTree (flat NavItems to a two-level tree, dropping inactive items, grandchildren and dead links), resolveNavUrl and resolvePageUrl, SYSTEM_PAGE_URL_OVERRIDES (page_key 'home' resolves to '/'), findUnlinkedPages (matches on page_key AND resolved URL), isNodeActive, isExternalUrl, toFallbackNodes, the DEFAULT_HEADER_ITEMS and DEFAULT_FOOTER_ITEMS fallbacks, and the NAV_LINK_TYPES and BUILT_IN_ROUTES admin pickers. |
| Home section helpers | utils/homeSections.js | Canonical list of the hardcoded Home sections (section_key and label), shared by Home.jsx which renders them and SitePageEditor which restores deleted ones via findMissingBuiltInSections. Adding an entry without a matching case in Home.jsx yields a block that renders nothing. |
| Event time helpers | utils/eventTime.js | parseTimeToMinutes converts the free-text Event.start_time / end_time strings ('7:00 PM', '19:00', '7 PM') into minutes since midnight, returning null for 'TBA' and anything unparseable. compareEventsByStart orders events within a single day: parsed start time is the primary key, sort_order only breaks ties between events starting at the same time, and null-time events sort last. compareEventsByDateThenStart adds date as the primary key for lists not already grouped by day, with undated events last. Shared by Events.jsx (day grouping), Home.jsx (featured strip selection and order) and EventDetails.jsx (Google Calendar and .ics export), so the schedule, the homepage and the calendar files can never interpret a time differently. Note that sort_order no longer controls day ordering on its own. |
| HTML sanitiser | utils/sanitizeHtml.js | sanitizeRichText wraps DOMPurify with an allowlist matching the ReactQuill toolbar (headings, bold/italic/underline/strike, lists, links) and blocks javascript:, data: and vbscript: URLs. External links are forced to rel='noopener noreferrer'. Note that '#text' must stay in ALLOWED_TAGS: without it DOMPurify silently strips all text and articles render blank. |
| Page templates | utils/pageTemplates.js | TEMPLATES with two starter block sets (about, sponsorship) used when creating a page, plus slugify. Note that TEMPLATES has no 'home' entry even though the template enum allows it. |
| PermissionsContext | contexts/PermissionsContext.jsx | Resolves an effective role (Base44 admin, else User.data.application_role, else 'user'), treats admin as super-admin, and otherwise loads granted permission_keys from RolePermission. |
| useHasPermission | hooks/useHasPermission.js | Returns true for super-admins, false while permissions load, otherwise checks the permission set. Used for manage_news only. |
| use-mobile | hooks/use-mobile.jsx | Viewport breakpoint hook supplied with the shadcn/ui setup. |
| AuthContext | lib/AuthContext.jsx | Platform auth context: loading states, auth errors, and navigateToLogin. Consumed by App.jsx and ProtectedRoute. |
| Base44 client | api/base44Client.js | Pre-initialised Base44 SDK client. Entity access is base44.entities.X with list, filter, create, update(id, data) and delete(id). |
| Query client | lib/query-client.js | TanStack Query client instance. Provided at the App root, though most components fetch directly through the SDK in useEffect rather than through Query. |
| Other lib/utils | lib/app-params.js · lib/utils.js · utils/index.ts | Platform and utility helpers, including the shadcn class-name helper. |
| Name | File Path / Value | Notes |
|---|---|---|
| Core.UploadFile | base44.integrations.Core.UploadFile | Image uploads. Called from fieldEditors (ImageUploadField), Events Admin, News Article Form and Page Settings. Consumes integration credits per upload. |
| Core.SendEmail | (not in use) | Not referenced anywhere in src/. Earlier revisions of this guide claimed the Sponsorship and Alumni forms send email; they write to entities instead. Form notifications are therefore not currently sent. |
| Name | File Path / Value | Notes |
|---|---|---|
| Homecoming logo | media.base44.com/.../dee9db58c_HC26SOCIALSTAMPS-2.png | Navbar logo, max height 50px. |
| Hero fallback image | media.base44.com/.../b38c7ebc3_HC26.png | Used only by the Home fallback layout. The live hero image is stored in the hero PageBlock content and is editable in the Page Builder. |
| Clawchella artwork | media.base44.com/.../d34c0c950_HC26Clawchella.png | Referenced in page content. |
| EndZone Experience artwork | media.base44.com/.../f2801771f_EndZoneExperience.png | Referenced in page content. |
| Uploaded block images | (per-record URLs) | Images added through the Page Builder, Events Admin or News are stored per record via Core.UploadFile and are not listed here. |
| Name | File Path / Value | Notes |
|---|---|---|
| Security posture | (reviewed 30 Jul 2026) | Security Scan clean as of 30 Jul 2026. Five Critical permission issues were closed by declaring explicit RLS on Event, FeaturedArtist, AlumniSubmission, SponsorshipSubmission and PageFormSubmission; the stored XSS was closed by sanitizeRichText; X-Frame-Options was enabled from the dashboard. Two rules of thumb: entity read must stay public for content the public site renders anonymously, and must stay admin-only for anything holding PII. Never use bulk 'Fix all issues' on this app, since a blanket lockdown of Event or FeaturedArtist read would take the public Events pages offline. |
| Permissions-Policy header | (dashboard: Security) | The scan also checks for Permissions-Policy, which limits access to camera, microphone and location. This app needs none of them, so enabling it costs nothing and reduces attack surface. |
| Marquee pause control | components/blocks/AnnouncementMarqueeBlock.jsx | Motion pauses on hover and keyboard focus and is disabled under prefers-reduced-motion. Strict WCAG 2.2 SC 2.2.2 also wants a visible pause control. HeroBlock now has one — a 44x44 gold-on-scrim toggle in the bottom-right corner — so copy that pattern here rather than inventing a second one. |
| Sponsor strip placeholders | pages/Home.jsx | The Sponsor Strip renders five dashed 'Sponsor Name' placeholder tiles. Acceptable pre-launch; hide the section from the Page Builder or replace it with real logos before launch. |
| Section background alternation | pages/Home.jsx | Built-in sections alternate #F9FAFB and #ffffff. Reordering them can place two same-coloured sections next to each other, which flattens the visual separation. Cosmetic only. |
| TEMPLATES vs template enum | utils/pageTemplates.js | Page.template allows 'home' but TEMPLATES defines only about and sponsorship. Intentional today, since 'home' exists only for the pre-existing record. |
| ProtectedRoute unused | components/ProtectedRoute.jsx | Exists but is not referenced by any route. Admin gating happens in AdminLayout instead. Either wire it up or remove it. |
B-CU Homecoming 2026 · Internal Reference · Not for public distribution