The Consistent Academy operates on a modern, decoupled serverless architecture with a dual-deployment setup and role-based access control.
- Frontend: React with TypeScript, built using Vite (
vite.config.ts). - Styling: Tailwind CSS (v3) with
shadcn/uifor accessible component primitives and a fully custom neumorphic design system insrc/index.css. - Backend Database & Auth: Firebase (Firestore + Firebase Authentication). Firestore is the single source of truth for all domain data.
- Media Storage: Cloudinary (dynamic uploads) and the
public/directory (static assets). Firebase Storage is not used. - Serverless Functions: A hybrid of Firebase Cloud Functions (
functions/src/) for Firestore-triggered or scheduled logic, and Vercel API routes (api/) for frontend-facing HTTP endpoints. - Deployment: Vercel (primary hosting, mapped via
vercel.json). Firebase Functions run on Firebase's own infrastructure but are called from the same frontend.
The project requires the following environment variables, stored in .env at the project root (never committed to version control):
| Variable | Purpose |
|---|---|
VITE_FIREBASE_API_KEY |
Firebase Web SDK authentication |
VITE_FIREBASE_AUTH_DOMAIN |
Firebase Auth domain |
VITE_FIREBASE_PROJECT_ID |
Firestore project identifier |
VITE_FIREBASE_STORAGE_BUCKET |
Storage bucket (Cloudinary is used instead for uploads) |
VITE_FIREBASE_MESSAGING_SENDER_ID |
Firebase Cloud Messaging sender |
VITE_FIREBASE_APP_ID |
Firebase App ID |
VITE_CLOUDINARY_CLOUD_NAME |
Cloudinary cloud (value: dftodlkkt) |
SENDGRID_API_KEY (or equivalent) |
Mailer service key, used server-side in api/contact.ts only |
All VITE_* prefixed variables are exposed to the client bundle. Non-prefixed variables are only available in Vercel serverless functions and must never be referenced in src/.
To ensure optimal performance and strict adherence to project pricing constraints, media handling is divided into two distinct pipelines:
Static, unchanging assets are stored locally within the repository in the public/ directory. This includes:
- UI/Brand Elements: Logos (
logo.png,logo2.png), developer credits (developers.png). - Educational Graphics: Course thumbnails (
public/courses/img1.png–img14.png) and assessment visuals (public/leadership/). - Literature Imagery: Book covers located in
public/books/. - Gallery Showcase: Hardcoded promotional event images found in
public/gallery/.
Crucial Architectural Rule: We do not use Firebase Storage. All dynamic media uploads—most notably images embedded within the Blog engine and the CC Club meeting video thumbnails—are processed and hosted through Cloudinary using the cloud name dftodlkkt.
- When an admin authors a blog post via
AdminBlogs.tsxor therich-text-editor.tsx, the image is pushed directly to Cloudinary. - When a Trainer uploads their own profile picture in
CCTrainerDashboard.tsx, it is pushed to Cloudinary viauploadToCloudinary()insrc/lib/utils.ts. - The resulting secure Cloudinary URL is then saved to the Firestore document as a string.
The platform relies on Firebase Firestore for scalable NoSQL data management. Below are the active collections and their architectural purposes.
Serves as the content repository for the public-facing blog (Blog.tsx, BlogPost.tsx) and the admin management portal (AdminBlogs.tsx).
- Schema Design:
id(Document ID): Unique identifier.title(String): The headline of the post.content(String/Rich Text): The body of the blog, containing HTML or markdown formatting from TipTap.coverImageUrl(String): The Cloudinary URL for the banner image.author(String): Name or ID of the author.createdAt(Timestamp): Publication date for sorting.tags(Array of Strings): For filtering and categorization.
Handles the persistence of data generated by the Assessment Engine (saveTestResult.ts). This is critical for tools like the Leadership Style Assessment and Grammar tests.
- Schema Design:
userId(String): Links the result to an authenticated user (or an anonymous session ID if unauthenticated).testType(String): e.g.,"leadership_assessment","grammar_kids".answers(Map/Object): The raw input provided by the user.calculatedScore/resultProfile(String/Number): The processed outcome (e.g.,"Transformational Leader").timestamp(Timestamp): When the assessment was completed.
The CC Club is a complex, role-based gamification and tracking module. Data is segmented across four collections to ensure security and logical separation.
Stores all user profiles interacting with the club (ccClub.ts).
uid(String): Maps to Firebase Authentication UID.name,email,college(String): User identity.club_role(String): One of"Student","President","Vice President","Team Leader","Event Team","Trainer". Drives routing and permission logic.current_level(Number, 1–4): The student's level in the CC Club curriculum.total_points(Number): Powers theCCGamificationPanel.tsxleaderboard display.badges(Array of Strings): Earned achievement badges.approvalStatus("pending"|"approved"): Used exclusively for the Trainer role — new Trainer accounts are gated until an admin approves them viaAdminDashboard.tsx.profilePictureUrl(String, optional): Cloudinary URL for Trainer profile photos.
Tracks individual student speech performance (CCSpeechTracker.tsx).
memberId(String): Reference to the speaker's UID.speechTitle(String): The topic.college(String): College scoping for multi-college support.level(Number): Which curriculum level this speech belongs to.workflowState(String): Tracks the speech through the President → Trainer review pipeline.dateDelivered(Timestamp): Historical tracking forCCProgressTimeline.tsx.evaluatorFeedback(String): Constructive notes provided by leadership.pointsAwarded(Number): Points contributed to the gamification engine.
Created by the President to document each weekly meeting.
college(String): College identifier.week_number(Number): Sequential meeting counter.meeting_date(Timestamp): When the meeting was held.agenda(String): What was covered.attendees(Array of Strings): UIDs of attending members — used to compute attendance percentages.meetingVideoUrl(String): Recording of the full meeting session (uploaded to Cloudinary by the President).
Tracks individual student video responses to each meeting report.
uid(String): The student who submitted.report_id(String): References thecc_meeting_reportsdocument.week_number(Number): Denormalized for efficient querying.videoUrl(String): Link submitted by the student (e.g., a Google Drive or YouTube link).status(String):"Pending Review","Approved","Redo","Trainer Evaluated".workflowState(String): Granular workflow step (see Section 5 for the full pipeline).
This is the most architecturally significant flow in the CC Club system. It describes how a student's meeting video travels through the three-tier review hierarchy before final evaluation.
[President Creates Meeting Report]
|
| createMeetingReport() → stores meeting in cc_meeting_reports
|
↓
[Students See the Report in their Dashboard]
|
| MeetingVideoPanel in CCStudentDashboard.tsx
| Student pastes a video URL (Google Drive / YouTube)
|
↓
[submitMeetingVideo() → cc_meeting_video_submissions]
| workflowState: "submitted_to_president"
| status: "Pending Review"
|
↓
[President Reviews in CCPresidentDashboard.tsx → VideoReviewPanel]
|
|── APPROVE ──→ validateMeetingVideo()
| workflowState: "submitted_to_trainer"
| status: "Pending Review"
| → toast: "Video approved & forwarded to Trainer"
|
|── REJECT ──→ rejectMeetingVideo()
workflowState: "rejected_by_president"
status: "Redo"
→ Student sees Redo notification; can resubmit
↓
[Student resubmits via resubmitSpeech()]
workflowState resets to "submitted_to_president"
|
↓ (after President approval)
[Trainer Reviews in CCTrainerDashboard.tsx]
|
| Trainer sees only "submitted_to_trainer" videos
| evaluateMeetingVideoByTrainer()
| Trainer fills: remarks, points for student, points for president
|
|── APPROVE ──→ workflowState: "trainer_evaluated"
| status: "Trainer Evaluated"
| pointsAwarded written to student's doc
| evaluatorFeedback stored
|
|── NEEDS REDO ─→ workflowState: "trainer_rejected"
status: "Redo"
→ Student is notified and can re-record
Why this three-tier design? The President validates basic quality (was it a real speech? right topic?), while the Trainer provides professional pedagogical evaluation (style, vocabulary, clarity). The President also earns points from the Trainer's evaluation, incentivizing them to review carefully.
Individual speeches follow a similar President → Trainer workflow via CCSpeechTracker.tsx:
- Student submits a speech record (title, date, level).
workflowState: "submitted_to_president"— President validates viavalidateSpeech()/rejectSpeech().- On President approval →
workflowState: "submitted_to_trainer"— Trainer evaluates viaevaluateSpeechByTrainer(). - On Trainer approval → points are awarded, feedback stored, and if 4 speeches are completed for the level,
current_levelincrements.
Trainer (pedagogical authority, evaluates all speech & video quality)
│
President (club operations — runs meetings, first-pass video review, creates reports)
│
Vice President, Team Leader, Event Team (operational roles with extended visibility)
│
Student (participates in speeches, submits videos, earns points)
- Routing logic in
ClubDashboard.tsx:isTrainer → CCTrainerDashboard,isPresident → CCPresidentDashboard, elseCCStudentDashboard. - Trainer approval gate: New Trainers are blocked from their dashboard until
approvalStatus === "approved"is set by an admin inAdminDashboard.tsx. - Data scoping: All Firestore queries in CC Club are scoped to
collegefield — enabling multi-campus expansion without data leakage.
Located within src/components/tests/ and src/lib/tests/, this system dynamically renders psychological or educational tests.
- Data Flow:
testData.tsandleadershipTestData.tsprovide the hardcoded questions, logic, and weighting.TestInterface.tsxorchestrates the UI flow. Upon completion, local state processes the exact outcome, hands it toLeadershipResults.tsxfor visual rendering, and simultaneously firessaveTestResult.tsto log the event in Firestore.
Instead of heavy backend servers, the app uses localized serverless routes. api/contact.ts serves as a dedicated Vercel function to securely process input from the Contact.tsx page, shielding any third-party mailing service credentials from the frontend client bundle.
There are two separate authentication contexts in this application — they share Firebase Auth but have distinct user profile structures:
| Context | Gate | User Doc Location | Routing |
|---|---|---|---|
| Main TCA Admin | AdminRoute.tsx checks Firebase ID Token for claims.admin === true |
Firebase ID Token claims (set via setAdmin.cjs / Firebase Admin SDK) |
/admin, /admin/blogs |
| CC Club Members | ClubPage.tsx checks Firebase Auth + Firestore cc_members/{uid} |
cc_members Firestore collection |
/club |
Key distinction: A user can have a Firebase Auth account without a CC Club profile. ClubPage.tsx explicitly handles this case — if getCCUser(uid) returns null (no Firestore record), the user is treated as unauthenticated and shown the sign-up form even though Firebase Auth succeeded.
PageLoader.tsx renders a full-screen branded animation while the main bundle initializes. Index.tsx gates all page content behind {!loading && ...} to prevent layout flash during the loader animation.
Custom fonts (Fredoka, and any editorial display fonts) are loaded via Google Fonts @import in src/index.css. Tailwind fontFamily config in tailwind.config.ts maps font-sans → DM Sans and font-display → Plus Jakarta Sans for component-level overrides.
components.json configures shadcn/ui with the project's custom radius and color tokens. New UI primitives should be added via bunx shadcn-ui@latest add <component> to maintain consistency.
src/
├── components/
│ ├── club/ CC Club feature components (scoped to .cc-club-scope)
│ │ ├── CCHero.tsx Landing hero for unauthenticated users
│ │ ├── CCAuthForm.tsx Login/signup for CC Club
│ │ ├── CCNavbar.tsx Authenticated nav (shows Back to TCA + logout)
│ │ ├── CCStudentDashboard.tsx Main student view (speeches, videos, timeline)
│ │ ├── CCPresidentDashboard.tsx First-pass review + meeting management
│ │ ├── CCTrainerDashboard.tsx Professional evaluation + analytics
│ │ ├── CCGamificationPanel.tsx Fixed side panel (points, badges, level)
│ │ ├── CCProgressTimeline.tsx 4-level progress indicator
│ │ ├── CCSpeechTracker.tsx Submit/track individual speeches
│ │ ├── CCMeetingPanel.tsx President creates meeting reports + attendance
│ │ └── CCInlineVideoPlayer.tsx Embedded video playback in dashboards
│ ├── layout/ Navbar, Footer, PageTransition
│ ├── tests/ Assessment engine components
│ └── ui/ shadcn/ui primitives + custom neumorphic components
├── pages/
│ ├── club/
│ │ ├── ClubPage.tsx Auth gate (loading/unauthenticated/authenticated)
│ │ └── ClubDashboard.tsx Role-based dashboard router
│ ├── Index.tsx Landing page
│ ├── AdminDashboard.tsx Admin portal (members, CC Club approvals)
│ └── ...
├── lib/
│ ├── firebase.ts Firebase SDK initialization
│ ├── ccClub.ts All CC Club Firestore operations + TypeScript types
│ ├── utils.ts Shared utilities (Cloudinary upload, cn(), etc.)
│ └── tests/ Test data and result calculation logic
├── index.css Neumorphic design system + CC Club dark scope
api/
└── contact.ts Vercel serverless function for contact form
functions/src/
└── index.ts Firebase Cloud Functions entry point