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.
The archived dash-and-static-DO-NOT-CODE branch contains useful portal UI and
data-model ideas, but it is not safe to merge directly. Its dashboard was only
guarded in the browser, its access-code cookies were writable by client code, a
profile endpoint accepted any user ID while using a service-role key, and its QR
flow used a fake token and a localhost-only Express server.
The reusable foundation now lives under /portal on main:
- Supabase SSR authentication with protected server-side routes
- Email/password login, password reset, and sign-out
- Invite/whitelist membership binding and required first-login onboarding
- A responsive dashboard shell based on the archived portal layout
- Member profile editing in an RLS-protected
portal_profilestable - Admin/exec strike logging with server-attributed audit records
- A separate Portal Supabase configuration, so the public Blog database is not reused for private member data
- No public self-signup and no client-editable role field
The navbar only shows PORTAL when both Portal environment variables are set:
NEXT_PUBLIC_PORTAL_SUPABASE_URL=
NEXT_PUBLIC_PORTAL_SUPABASE_ANON_KEY=
Resume the KTP Portal Supabase project before adding these values in Vercel.
Run supabase/portal-schema.sql, then in Supabase Auth settings add
https://ktp-website.vercel.app/portal/auth/callback as an allowed redirect
URL and disable open user signups. Provision member accounts through Supabase
invites after adding each lower-case email to portal_members.
Events, RSVP, attendance, QR check-in, and admin filters from the archived
branch still require their own schema and row-level security review. They are
intentionally not copied as direct browser database writes. See
docs/PORTAL.md.
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.