A full-stack web application that centralises club recruitment drives and event announcements for college campuses. Instead of relying on scattered WhatsApp groups where important deadlines get buried, ClubIndex gives every club a verified space to post recruitments and events and gives students a single feed to browse, search, and apply.
- Browse Recruitments — paginated feed of open recruitment posts with filters by audience (WC / MC / Other), club type, and free-text search.
- Browse Events — same experience for campus events (Juniors / Seniors / Other).
- One-Click Apply — each post links directly to the club's application form; clicks are tracked for analytics.
- Post Management — create, edit, and delete recruitment & event posts for your club.
- Club Profile — update club name, photo, and type.
- Click Analytics — per-post click charts showing engagement over time.
- Signup Tokens — administrators generate one-time tokens that let new club admins register.
- Full Control — create, edit, and delete any club; moderate all posts.
- Token Management — generate and revoke signup tokens for any club.
| Layer | Technology |
|---|---|
| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS 4, Turbopack |
| Backend | Express 5, TypeScript, Node.js |
| Database | PostgreSQL (via pg) |
| Auth | JWT access + refresh tokens, bcrypt password hashing, HTTP-only cookies |
clubIndex/
├── backend/ # Express API server
│ ├── src/
│ │ ├── index.ts # Server entry point
│ │ ├── app.ts # Express app setup & route mounting
│ │ ├── controllers/
│ │ │ ├── auth/ # Login, token refresh, sign-out logic
│ │ │ ├── events/ # Event CRUD controller
│ │ │ ├── recruitments/ # Recruitment CRUD controller
│ │ │ ├── services/ # Shared service helpers
│ │ │ ├── clubController.ts
│ │ │ ├── clickController.ts
│ │ │ └── signUpTokenController.ts
│ │ ├── routes/
│ │ │ ├── auth/ # login, refresh, signout routes
│ │ │ ├── club.routes.ts
│ │ │ ├── event.routes.ts
│ │ │ ├── recruitment.routes.ts
│ │ │ ├── click.routes.ts
│ │ │ └── signUpToken.routes.ts
│ │ ├── middleware/
│ │ │ ├── auth.ts # JWT verification middleware
│ │ │ └── clickLimiter.ts # Rate-limits clicks (1 per post/IP/day)
│ │ ├── db/
│ │ │ ├── db.ts # PostgreSQL connection pool
│ │ │ ├── schema.sql # Table definitions
│ │ │ └── setup.sql # Full DB reset + seed script
│ │ ├── types/ # TypeScript type declarations
│ │ └── lib/ # Shared utilities
│ ├── .env.example
│ ├── nodemon.json
│ ├── package.json
│ └── tsconfig.json
│
├── club-index/ # Next.js frontend
│ ├── app/
│ │ ├── page.tsx # Landing page
│ │ ├── layout.tsx # Root layout (dark theme, Geist fonts)
│ │ ├── globals.css
│ │ ├── recruitments/ # /recruitments — paginated recruitment feed
│ │ ├── events/ # /events — paginated event feed
│ │ └── manage/
│ │ ├── page.tsx # Admin dashboard (club admin / administrator)
│ │ └── analytics/ # /manage/analytics/[type]/[postId] — click charts
│ ├── components/
│ │ ├── cards/ # PostCard, EventCard, RecruitmentCard, ClubCard
│ │ ├── forms/ # AuthModal, ClubForm, EventForm, RecruitmentForm
│ │ ├── layout/ # Header, Footer, Panels, Tabs, Pagination, etc.
│ │ └── ui/ # Badge, Button, Input, Select, TextArea
│ ├── contexts/
│ │ ├── AuthContext.tsx # Auth state + JWT refresh logic
│ │ └── Providers.tsx
│ ├── lib/
│ │ ├── api.ts # Typed API client with auto-refresh on 401
│ │ ├── hooks/ # usePaginatedData, etc.
│ │ ├── richText.tsx
│ │ └── utils.ts
│ ├── types/
│ │ └── api.ts # Shared frontend type definitions
│ ├── public/ # Static assets
│ ├── package.json
│ └── tsconfig.json
│
├── .gitignore
└── README.md
The PostgreSQL database uses the following tables:
| Table | Purpose |
|---|---|
clubs |
Club profiles (name, photo, type) |
recruitment_posts |
Recruitment ads with deadline, form link, tags, and audience (WC/MC/Other) |
event_posts |
Event announcements with similar metadata |
club_admins |
Club-scoped admin accounts (bcrypt-hashed passwords) |
administrators |
Platform-wide super-admin accounts |
club_signup_tokens |
One-time tokens for registering new club admin accounts |
refresh_tokens |
JWT refresh tokens for persistent sessions |
recruitment_clicks |
Click tracking for recruitment posts |
event_clicks |
Click tracking for event posts |
Notable constraints:
- Each club can have at most one active MC recruitment post and at most one active WC recruitment post (enforced via partial unique indexes).
- Tags are stored as
JSONBwith a GIN index for fast filtering.
All routes are prefixed with /api.
| Method | Route | Auth | Description |
|---|---|---|---|
POST |
/login/login |
— | Authenticate (returns access token + refresh cookie) |
PUT |
/login/login |
— | Register a new club admin (requires signup token) |
POST |
/refresh/refresh |
Cookie | Refresh the access token |
POST |
/signout/signout |
JWT | Invalidate refresh token |
GET |
/recruitment/recruitment/:page |
— | Paginated recruitments (supports query filters) |
POST |
/recruitment/recruitment |
JWT | Create recruitment post |
PUT |
/recruitment/recruitment/:id |
JWT | Update recruitment post |
DELETE |
/recruitment/recruitment/:id |
JWT | Delete recruitment post |
PUT |
/recruitment/recruitment/:id/click |
— | Record a click (rate-limited) |
GET |
/recruitment/recruitment/:id/clicks |
JWT | Get click history |
GET |
/event/events/:page |
— | Paginated events (supports query filters) |
POST |
/event/events |
JWT | Create event post |
PUT |
/event/events/:id |
JWT | Update event post |
DELETE |
/event/events/:id |
JWT | Delete event post |
PUT |
/event/events/:id/click |
— | Record a click (rate-limited) |
GET |
/event/events/:id/clicks |
JWT | Get click history |
GET |
/club/club/list |
— | List all clubs |
GET |
/club/club |
JWT | Get current admin's club |
POST |
/club/club |
JWT | Create a club (admin only) |
PUT |
/club/club/:id |
JWT | Update club |
DELETE |
/club/club/:id |
JWT | Delete club |
PUT |
/signup-token/signup-token/:club_id |
JWT | Generate signup token |
DELETE |
/signup-token/signup-token/:token_id |
JWT | Revoke signup token |
- Node.js ≥ 18
- PostgreSQL ≥ 14
git clone https://github.com/<your-username>/clubIndex.git
cd clubIndex# Connect to psql and create a database
psql -U postgres -c "CREATE DATABASE clubindex;"
# Run the setup script (creates tables + seeds a default admin)
psql -U postgres -d clubindex -f backend/src/db/setup.sqlcd backend
cp .env.example .env
# Edit .env with your DB credentials and a strong JWT_SECRET
npm installcd club-index
npm installCreate a .env.local file:
NEXT_PUBLIC_API_URL=http://localhost:3000
In two separate terminals:
# Terminal 1 — Backend
cd backend
npm run dev # starts nodemon with ts-node on port 3000
# Terminal 2 — Frontend
cd club-index
npm run dev # starts Next.js with Turbopack on port 3001| Variable | Description | Default |
|---|---|---|
PORT |
Server port | 3000 |
NODE_ENV |
Environment | development |
JWT_SECRET |
Secret for signing JWTs | — |
DB_USER |
PostgreSQL user | postgres |
DB_HOST |
PostgreSQL host | localhost |
DB_NAME |
Database name | — |
DB_PASSWORD |
Database password | — |
DB_PORT |
PostgreSQL port | 5432 |
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_API_URL |
Backend API base URL | http://localhost:3000 |
This project is licensed under the GNU General Public License v3.0.