First, install all necessary dependencies:
npm i
# or
npm installNext, run the development server:
npm run devOpen http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.js. The page auto-updates as you edit the file.
This project uses next/font to automatically optimize and load Inter, a custom Google Font.
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.
The contact form (/contact-us) sends messages via EmailJS (client-side, in components/SubmitButton.jsx). If EmailJS fails (for example, its Gmail OAuth grant expires), the form automatically falls back to POSTing to app/api/contact/route.js, which sends via Resend so submissions aren't lost.
- EmailJS: if the form stops working with a "Gmail connection" / 412 error, reconnect the Gmail account in the EmailJS dashboard (Email Services → the Gmail service → Reconnect). Gmail OAuth grants expire periodically.
- Resend fallback: set the
RESEND_API_KEYenv var in Vercel to enable it. Create a free key at resend.com using the KTP Gmail as the account email (lets it send to that inbox fromonboarding@resend.devwithout domain verification). Without the key, the fallback simply no-ops.
The Recruitment page shows an "Upcoming Events" section that is managed entirely from Supabase, so no code change or redeploy is needed to add or remove an event.
- Code: UI in
components/EventsSection.jsx; server route inapp/api/events/route.js. Events live in aneventstable in the KTP Blog Supabase project. - Add an event: Supabase dashboard → KTP Blog project → Table Editor →
events→ Insert row. Settitleandevent_date(use Central time);location,description, andrsvp_urlare optional. It shows on the site immediately. - Remove / hide an event: set the row's
is_activetofalseto hide it (reversible), or Delete the row to remove it permanently. Past events drop off automatically by date. - Only active, upcoming events are shown, and the section hides itself when there are none. See
docs/EVENTS.mdfor full details and the one-time setup SQL. - These events also feed the AI chatbot, so it can answer "what events are coming up?" from the same live list (updates within about 5 minutes).
The home page shows a "Happy Birthday" banner on days when a brother has a birthday. Like events, birthdays are managed entirely from Supabase, so no code change or redeploy is needed.
- Code: UI in
components/BirthdayBanner.jsx; server route inapp/api/birthdays/route.js. Birthdays live in abirthdaystable in the KTP Blog Supabase project. - Privacy: only the month and day are stored (no birth year), so nobody's age is shown.
- Add a birthday: Supabase dashboard → KTP Blog project → Table Editor →
birthdays→ Insert row. Setname,birth_month(1-12), andbirth_day(1-31). - Remove / hide a birthday: set
is_activetofalseto hide it (reversible), or Delete the row. - The banner only appears on someone's actual birthday and hides itself otherwise. See
docs/BIRTHDAYS.mdfor full details and the one-time setup SQL.
The site has a floating AI chat assistant (bottom-right, on every page) powered by the Google Gemini API.
- Code: UI in
components/Chatbot.jsx; server route inapp/api/chat/route.js. The API key is read server-side and is never exposed to the browser. - API key: stored in Vercel as the
GEMINI_API_KEYenvironment variable (Project → Settings → Environment Variables). It is not committed to the repo. - Cost: uses the Gemini free tier. As long as no billing account is attached to the Google Cloud project, it cannot be charged; at the free limit, requests just return an error.
Gemini API keys don't expire. You only need to replace the key if it is leaked publicly, abused, or you want it on a different (chapter-owned) Google account.
How you'll know there's a key problem: the chatbot consistently replies with its fallback line ("…email kappathetapiutd@gmail.com") instead of real answers. The route degrades gracefully, so a bad/missing key never breaks the site.
To replace it (~2 min):
- Create a new key at https://aistudio.google.com/apikey (use a personal 18+ Google account; the chapter Gmail is age-blocked from AI Studio).
- In Vercel → KTPWebsite → Settings → Environment Variables, edit
GEMINI_API_KEYwith the new value and Save. - Redeploy (Deployments → ⋯ → Redeploy).
- Optionally delete the old key in AI Studio.
Optional env override: GEMINI_MODEL (defaults to gemini-2.5-flash-lite).
Beyond its built-in KTP facts and the live board roster (lib/roster.js), the chatbot pulls extra knowledge from a knowledge table in the KTP Blog Supabase project (lib/knowledge.js). Rows are injected into the AI's context using lightweight keyword retrieval with a character budget, so large documents (e.g. the constitution) only load when relevant.
- Add/edit knowledge: Supabase dashboard → KTP Blog project → Table Editor →
knowledge→ add a row (title,content). Setis_activeto false to hide a row. Changes appear in the bot within ~5 minutes (cache); no redeploy needed. - Only store PUBLIC-safe info. The chatbot speaks it to any visitor and the table is readable via the public anon key. Do not store private member data or anything confidential.
- The current board roster is the sole source of truth for who holds a position; names appearing in stored documents are treated as historical.
Google Analytics 4 is wired in via components/GoogleAnalytics.jsx. The Measurement ID (G-…, which is public by design) is set as a default in code and can be overridden per-environment with the NEXT_PUBLIC_GA_ID env var.
Copy .env.example to .env for local development. The real .env file is
ignored by Git and must never be committed.
.env.production contains only the Supabase URL and anonymous browser key,
which are public by design and already shipped to every visitor. It exists as a
deployment fallback so Vercel builds do not depend on a previously committed
private .env. Private keys must never be added to .env.production.
The Cloudinary upload helper reads CLOUDINARY_CLOUD_NAME,
CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET from .env:
npm run upload-headshots -- "C:\Users\YourName\Downloads" --dry-run
npm run upload-headshots -- "C:\Users\YourName\Downloads"The Cloudinary secret was historically committed before the environment-based
configuration was added. Rotate it in Cloudinary even though the current source
file no longer contains it. See docs/SECURITY.md for the
access-handoff, credential-rotation, member-data, and incident-response rules.