Official web platform of the Computer Science Society (CSS) at the International Islamic University, Islamabad.
Visit Now : https://cssiiui.vercel.app/
- Purpose
- Tech Stack
- Architecture Overview
- Database Schema (ERD)
- Data Flow
- API Reference
- Sequence Diagrams
- Frontend Structure
- Design System
- Directory Map
- Setup & Installation
- Admin Panel
The CS Society portal is a full-stack web application that serves as the digital home for the Computer Science Society at IIUI. It provides:
- Public pages — homepage, events calendar, team roster, alumni directory, photo gallery, about, and contact form.
- Admin CMS — a protected dashboard where society executives can manage all content (events, news, team members, alumni, gallery images) without writing code.
- Media pipeline — image upload, storage, and delivery through Cloudinary CDN.
- Contact system — inquiry form backed by Resend email API with a simulation fallback for development.
| Technology | Version | Role |
|---|---|---|
| Next.js | 15.4.6 | Full-stack React framework (App Router) |
| React | 19.1.0 | UI component library |
| Node.js | 18+ | Server runtime |
| Technology | Role |
|---|---|
| PostgreSQL (Neon) | Cloud relational database |
| pg (node-postgres) | Raw SQL query driver via connection pool |
| Cloudinary | Media storage and CDN delivery |
| Package | Version | Purpose |
|---|---|---|
pg |
8.16.3 | PostgreSQL connection pool for executing raw SQL queries against Neon |
bcrypt |
6.0.0 | One-way password hashing (blowfish) for admin authentication |
cloudinary |
2.10.0 | Server-side SDK for image upload streams and asset deletion |
next-cloudinary |
6.17.5 | React component wrappers for Cloudinary browser-side uploads |
react-icons |
4.12.0 | SVG icon library (FontAwesome, Ionicons) for UI elements |
| Technology | Role |
|---|---|
| Tailwind CSS 3.4 | Utility-first CSS framework |
| Custom CSS Variables | Obsidian design tokens defined in globals.css |
| Inter | Primary sans-serif typeface (Google Fonts) |
| JetBrains Mono | Monospace typeface for labels and code elements |
| Package | Role |
|---|---|
| ESLint + eslint-config-next | Code linting |
| PostCSS + Autoprefixer | CSS processing pipeline |
| Turbopack | Development server bundler (next dev --turbopack) |
The application follows a monolithic architecture within the Next.js App Router. Frontend pages, API route handlers, and server components coexist in a single deployment.
graph TD
Browser["Browser Client"]
NextServer["Next.js Server"]
Pool["pg.Pool Connection"]
NeonDB[("Neon PostgreSQL")]
CDN["Cloudinary CDN"]
Resend["Resend Email API"]
Browser -- "HTTP Requests" --> NextServer
NextServer -- "Raw SQL Queries" --> Pool
Pool -- "SSL Connection" --> NeonDB
Browser -- "Direct Image Upload" --> CDN
NextServer -- "Stream Upload / Delete" --> CDN
NextServer -- "Email Dispatch" --> Resend
-
Pure Raw SQL — The project runs purely on raw SQL queries using the connection pool from the Node Postgres
pgdriver. There is no runtime ORM (no Prisma, no Sequelize) to ensure maximum database performance, full control, and query transparency. -
Server-side API boundaries — API routes execute on the server, keeping environment credentials (database URLs, API secrets) out of client bundles.
-
Decoupled media storage — Images are uploaded to Cloudinary. Only the resulting CDN URLs are stored in the database. This keeps the database lightweight and backups small.
-
Client-side rendering for data pages — Most public pages use
'use client'withuseEffect+fetchto load data on mount. This keeps the initial page shell fast while data hydrates. -
PostgreSQL case sensitivity — The database uses PascalCase table names and camelCase columns. All raw SQL queries use double-quoted identifiers (e.g.,
"TeamMember","gradYear") to match the PostgreSQL case-sensitive system correctly.
The database contains 6 tables with one foreign-key relationship between Image and Event.
erDiagram
ADMIN {
int id PK "Auto-increment"
string email "Unique login identifier"
string name "Display name"
string password "bcrypt hash"
datetime createdAt
datetime updatedAt
}
EVENT {
int id PK "Auto-increment"
string title "Event name"
text description "Rich-text body"
datetime date "Scheduled date"
string locationType "OFFLINE | ONLINE"
string venue "Physical address or URL"
string eventType "Workshop | Seminar | Hackathon | Speaker Session | Other"
string registrationLink "External registration link"
datetime createdAt
datetime updatedAt
}
IMAGE {
int id PK "Auto-increment"
string url "Cloudinary CDN URL"
string caption "Optional description"
int eventId FK "Nullable"
datetime createdAt
}
NEWS {
int id PK "Auto-increment"
string title "Headline"
text details "Body content"
datetime date "Publication date"
string imageUrl "Cover photo URL"
datetime createdAt
datetime updatedAt
}
TEAM_MEMBER {
int id PK "Auto-increment"
string name "Full name"
string designation "Role title"
text details "Short bio"
string imageUrl "Portrait URL"
string instagram "Handle"
string linkedin "Profile URL"
string facebook "Profile URL"
datetime createdAt
datetime updatedAt
}
ALUMNI {
int id PK "Auto-increment"
string name "Full name"
string gradYear "Batch e.g. 2024"
string company "Employer"
string role "Job title"
string imageUrl "Portrait URL"
string linkedin "Profile URL"
int priority "Sort weight, lower is first"
datetime createdAt
datetime updatedAt
}
EVENT ||--o{ IMAGE : "has many"
- Event → Image — one-to-many. Each event can have multiple gallery images.
Image.eventIdis a nullable foreign key; when set, the image belongs to that event. WhenNULL, the image is a standalone gallery asset. - Cascade delete — deleting an Event automatically deletes all its associated Images (both from the database and from Cloudinary).
- All other tables are independent with no foreign-key constraints.
sequenceDiagram
participant B as Browser
participant N as Next.js Server
participant API as API Route Handler
participant DB as PostgreSQL
B->>N: GET /events (page request)
N-->>B: HTML shell + JS bundle
B->>API: GET /api/events (client fetch)
API->>DB: SELECT * FROM "Event" ORDER BY "date" DESC
DB-->>API: Rows
API->>DB: SELECT * FROM "Image" WHERE "eventId" = $1 (per event)
DB-->>API: Image rows
API-->>B: JSON array with nested images
B->>B: Render event cards
sequenceDiagram
participant A as Admin Browser
participant API as /api/upload-url
participant C as Cloudinary
A->>A: Select file from disk
A->>API: POST FormData { file }
API->>API: Read file → ArrayBuffer → Buffer
API->>API: Generate random hex filename
API->>C: upload_stream(buffer, folder: "css_iiui")
C-->>API: { secure_url }
API-->>A: { url: secure_url }
A->>A: Store URL for subsequent API call
sequenceDiagram
participant A as Admin Browser
participant Upload as /api/upload-url
participant Events as /api/events
participant C as Cloudinary
participant DB as PostgreSQL
A->>Upload: POST FormData { file_1 }
Upload->>C: Stream upload
C-->>Upload: { secure_url_1 }
Upload-->>A: url_1
A->>Upload: POST FormData { file_2 }
Upload->>C: Stream upload
C-->>Upload: { secure_url_2 }
Upload-->>A: url_2
A->>Events: POST { title, description, date, images: [url_1, url_2] }
Events->>DB: INSERT INTO "Event" ... RETURNING *
DB-->>Events: event row
Events->>DB: INSERT INTO "Image" (url, eventId) ... (per URL)
DB-->>Events: image rows
Events-->>A: { event + images }
A->>A: Redirect to /admin/events
sequenceDiagram
participant A as Admin Browser
participant API as /api/events/[id]
participant DB as PostgreSQL
participant C as Cloudinary
A->>API: DELETE /api/events/5
API->>DB: SELECT * FROM "Event" WHERE "id" = 5
DB-->>API: Event row (or 404)
API->>DB: SELECT * FROM "Image" WHERE "eventId" = 5
DB-->>API: Image rows
loop Each image
API->>C: destroy(public_id)
C-->>API: ok
end
API->>DB: DELETE FROM "Image" WHERE "eventId" = 5
API->>DB: DELETE FROM "Event" WHERE "id" = 5
API-->>A: { ok: true }
sequenceDiagram
participant B as Browser
participant API as /api/contact
participant R as Resend API
B->>API: POST { name, email, subject, message }
alt RESEND_API_KEY is set
API->>R: POST /emails { from, to, subject, html }
R-->>API: { id }
API-->>B: { ok: true, id }
else No API key (development)
API->>API: Console log email content
API-->>B: { ok: true, simulated: true }
end
sequenceDiagram
participant A as Admin Browser
participant Login as /api/auth/login
participant Me as /api/auth/me
participant Logout as /api/auth/logout
participant DB as PostgreSQL
A->>Login: "POST { email, password }"
Login->>DB: "SELECT * FROM Admin WHERE email = $1"
DB-->>Login: Admin row
Login->>Login: bcrypt.compare(password, hash)
Login-->>A: "Set-Cookie: admin=1 (HttpOnly, SameSite=Strict, 24h)"
A->>A: Redirect to /admin
Note over A,Me: On every admin page load / route change
A->>Me: GET /api/auth/me
Me->>Me: "Parse cookie header for admin=1"
Me-->>A: "{ admin: true/false }"
alt Not authenticated
A->>A: Redirect to /admin/login
end
Note over A,Logout: On admin logout click
A->>Logout: POST /api/auth/logout
Logout-->>A: "Set-Cookie: admin=; Max-Age=0 (deletes cookie)"
A->>A: Redirect to /admin/login
Base URL: /api
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/login |
Authenticate with username + password. Sets admin=1 cookie. |
| POST | /auth/logout |
Clears the admin cookie. |
| GET | /auth/me |
Returns { admin: true/false } based on cookie. |
| POST | /auth/change-credentials |
Update admin username, name, and password. Requires auth. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /events |
List all events with nested images, sorted by date descending. |
| POST | /events |
Create an event. Body: { title, description?, date?, locationType?, venue?, eventType?, images?[] } |
| DELETE | /events/[id] |
Delete event and cascade-delete associated images from DB and Cloudinary. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /news |
List all news articles, sorted by date descending. |
| POST | /news |
Create article. Body: { title, details, imageUrl?, date? } |
| GET | /news/[id] |
Fetch a single news article. |
| PUT | /news/[id] |
Update article. Body: { title?, details?, imageUrl?, date? } |
| DELETE | /news/[id] |
Delete article and remove cover image from Cloudinary. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /team |
List all team members, sorted by ID ascending. |
| POST | /team |
Create member. Body: { name, designation, details?, imageUrl?, instagram?, linkedin?, facebook? } |
| PUT | /team/[id] |
Update member. |
| DELETE | /team/[id] |
Delete member and remove portrait from Cloudinary. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /alumni |
List all alumni, sorted by priority ascending then grad year descending. |
| POST | /alumni |
Create alumni. Body: { name, gradYear, company?, role?, imageUrl?, linkedin?, priority? } |
| PUT | /alumni/[id] |
Update alumni. |
| DELETE | /alumni/[id] |
Delete alumni and remove portrait from Cloudinary. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /gallery |
List all images with event titles via LEFT JOIN, sorted by ID descending. |
| POST | /gallery |
Register an image. Body: { url, caption?, eventId? } |
| DELETE | /gallery/[id] |
Delete image record and remove asset from Cloudinary. |
| Method | Endpoint | Description |
|---|---|---|
| POST | /upload-url |
Upload a file to Cloudinary. Body: FormData { file }. Returns { url }. |
| POST | /contact |
Send a contact inquiry. Body: { name, email, subject, message }. |
All resource endpoints (team, alumni, news, gallery) follow the same pattern:
Create:
- Admin uploads image via
POST /api/upload-url→ receives Cloudinary URL - Admin submits form via
POST /api/{resource}with the URL in the payload - Server inserts row with
INSERT INTO ... RETURNING * - Client receives created object and refreshes the list
Update:
- Admin modifies fields and optionally uploads a new image
PUT /api/{resource}/[id]fetches the existing row- If
imageUrlchanged, the old Cloudinary asset is deleted viadeleteObject() - Row is updated with
UPDATE ... SET ... WHERE "id" = $1 RETURNING *
Delete:
DELETE /api/{resource}/[id]fetches the existing row- Cloudinary asset is deleted if a URL exists
- Row is deleted with
DELETE FROM ... WHERE "id" = $1
When deleting a Cloudinary asset, the server extracts the public_id from the URL:
Input: https://res.cloudinary.com/cloud/image/upload/v123456/css_iiui/abc123.jpg
───────────────────────
Step 1: Split on "/upload/" → "v123456/css_iiui/abc123.jpg"
Step 2: Strip version prefix → "css_iiui/abc123.jpg"
Step 3: Remove extension → "css_iiui/abc123"
Result: public_id = "css_iiui/abc123"
| Route | Component | Rendering | Description |
|---|---|---|---|
/ |
page.js |
Server | Homepage: Hero, Events, Team, FAQ |
/events |
events/page.jsx |
Client | Events grid with cards |
/events/[id] |
events/[id]/page.jsx |
Server (async) | Event detail with image slideshow |
/team |
team/page.jsx |
Client | Team member grid with social links |
/alumni |
alumni/page.jsx |
Client | Alumni directory sorted by priority |
/gallery |
gallery/page.jsx |
Client | Image grid with CORS-safe download |
/about |
about/page.jsx |
Server | Static about page with timeline |
/contact |
contact/page.jsx |
Client | Contact form (Resend integration) |
/admin |
admin/page.jsx |
Client | Dashboard with 6 management cards |
/admin/login |
admin/login/page.jsx |
Client | Login form |
/admin/events |
admin/events/page.jsx |
Client | Event list with create/delete |
/admin/events/new |
admin/events/new/page.jsx |
Client | Event creator with rich editor |
/admin/team |
admin/team/page.jsx |
Client | Team CRUD with inline form |
/admin/alumni |
admin/alumni/page.jsx |
Client | Alumni CRUD with inline form |
/admin/gallery |
admin/gallery/page.jsx |
Client | Gallery upload and management |
/admin/news |
admin/news/page.jsx |
Client | News CRUD with inline form |
/admin/settings |
admin/settings/page.jsx |
Client | Change credentials |
| Component | Type | Data Source | Description |
|---|---|---|---|
Navbar.jsx |
Client | None | Sticky header with scroll-aware glassmorphism, mobile hamburger |
Hero.jsx |
Client | GET /api/news |
Consolidated landing and news auto-rotating slideshow (8s interval) using latest news |
EventsSection.jsx |
Client | GET /api/events |
Horizontal snap-scroll carousel of event cards |
EventCard.jsx |
Shared | Props | Reusable event card with image, metadata, and detail link |
CoreTeamSection.jsx |
Client | GET /api/team |
Core team section showcasing president and team leads |
TeamCard.jsx |
Shared | Props | Reusable team member profile card with socials |
FAQSection.jsx |
Client | Hardcoded | Accordion with 4 Q&A items |
EventSlideshow.jsx |
Client | Props | Image carousel with dots and crossfade transitions |
Footer.jsx |
Server | None | Logo, Instagram, LinkedIn, Discord channel link, contact link |
The visual identity uses an obsidian-zinc dark theme defined through CSS custom properties in globals.css.
| Token | Value | Usage |
|---|---|---|
--bg |
#09090b |
Page background |
--fg |
#f4f4f5 |
Primary text |
--muted |
#71717a |
Secondary text, labels |
--border |
#18181b |
Card and section borders |
--surface |
#0f0f11 |
Card backgrounds |
--surface-hover |
#151518 |
Card hover state |
| Font | Weight Range | Usage |
|---|---|---|
| Inter | 300–900 | Body text, headings, UI elements |
| JetBrains Mono | 400, 700 | Labels, tags, monospace accents |
| Class | Purpose |
|---|---|
.section |
Max-width container (6xl) with responsive padding |
.section-title |
Responsive heading (2–3rem), 800 weight, uppercase |
.label |
Monospace micro-label with auto [bracket] decoration |
.card |
Surface-colored card with border and hover transition |
.btn |
Primary button: white fill, inverts on hover |
.btn-ghost |
Ghost button: transparent fill, border brightens on hover |
.slider-nav |
Positioned arrow buttons for carousels |
.no-scrollbar |
Hides scrollbar across all browsers |
.animate-fade-in-up |
Entry animation: fade + slide up (0.7s) |
.tech-grid |
Radial dot-grid background pattern (30px spacing) |
css_iiui/
├── public/
│ └── favicon-16x16.png # Site favicon
│
├── scripts/
│ └── db-setup.mjs # Database schema creation & seeding
│
├── src/
│ ├── app/
│ │ ├── layout.js # Root layout (fonts, navbar, footer)
│ │ ├── page.js # Homepage
│ │ ├── globals.css # Design system tokens and utilities
│ │ │
│ │ ├── about/page.jsx # About page with timeline
│ │ ├── alumni/page.jsx # Alumni directory
│ │ ├── contact/page.jsx # Contact form
│ │ ├── events/
│ │ │ ├── page.jsx # Events listing
│ │ │ └── [id]/page.jsx # Event detail (server-rendered)
│ │ ├── gallery/page.jsx # Photo gallery with download
│ │ ├── team/page.jsx # Team roster
│ │ │
│ │ ├── admin/
│ │ │ ├── layout.jsx # Auth guard wrapper
│ │ │ ├── page.jsx # Dashboard
│ │ │ ├── login/page.jsx # Login form
│ │ │ ├── settings/page.jsx # Credential management
│ │ │ ├── events/
│ │ │ │ ├── page.jsx # Events manager
│ │ │ │ └── new/page.jsx # Event creator
│ │ │ ├── team/page.jsx # Team CRUD
│ │ │ ├── alumni/page.jsx # Alumni CRUD
│ │ │ ├── gallery/page.jsx # Gallery CRUD
│ │ │ └── news/page.jsx # News CRUD
│ │ │
│ │ └── api/
│ │ ├── auth/
│ │ │ ├── login/route.js
│ │ │ ├── logout/route.js
│ │ │ ├── me/route.js
│ │ │ └── change-credentials/route.js
│ │ ├── events/
│ │ │ ├── route.js
│ │ │ └── [id]/route.js
│ │ ├── news/
│ │ │ ├── route.js
│ │ │ └── [id]/route.js
│ │ ├── team/
│ │ │ ├── route.js
│ │ │ └── [id]/route.js
│ │ ├── alumni/
│ │ │ ├── route.js
│ │ │ └── [id]/route.js
│ │ ├── gallery/
│ │ │ ├── route.js
│ │ │ └── [id]/route.js
│ │ ├── upload-url/route.js
│ │ └── contact/route.js
│ │
│ ├── assets/
│ │ └── css.png # Society logo
│ │
│ ├── components/
│ │ ├── Navbar.jsx
│ │ ├── Hero.jsx
│ │ ├── EventsSection.jsx
│ │ ├── EventCard.jsx
│ │ ├── CoreTeamSection.jsx
│ │ ├── TeamCard.jsx
│ │ ├── FAQSection.jsx
│ │ ├── EventSlideshow.jsx
│ │ ├── Footer.jsx
│ │ └── admin/
│ │ └── EventEditor.jsx
│ │
│ └── lib/
│ ├── db.js # PostgreSQL connection pool
│ ├── cloudinary.js # Upload, delete, random name helpers
│ └── adminAuth.js # Cookie-based auth check
│
├── .env # Environment variables (git-ignored)
├── .gitignore
├── package.json
├── tailwind.config.js
├── postcss.config.js
├── next.config.mjs
└── eslint.config.mjs
- Node.js 18 or above — nodejs.org
- PostgreSQL database — Neon (cloud) or local instance
- Cloudinary account — cloudinary.com
git clone https://github.com/Abubakkar-Khan/css_iiui.git
cd css_iiui
npm installCreate a .env file in the project root:
# PostgreSQL connection string
DATABASE_URL="postgresql://USER:PASSWORD@HOST/DATABASE?sslmode=require"
# Cloudinary credentials
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="your_cloud_name"
NEXT_PUBLIC_CLOUDINARY_API_KEY="your_api_key"
CLOUDINARY_API_SECRET="your_api_secret"
# Optional: Resend email API key (omit for simulation mode)
# RESEND_API_KEY="re_your_key_here"Initialize the tables and seed the default admin account:
node scripts/db-setup.mjsThis script connects to your PostgreSQL instance using the DATABASE_URL environment variable, creates all 6 tables if they do not exist, and seeds the default administrator account.
npm run devOpen http://localhost:3000 in your browser.
npm run build
npm startAll schema changes are handled via standard SQL operations (such as executing ALTER TABLE statements directly). You can use Node.js scripts using the pg pool driver to apply schema updates or migrations.
Access the admin dashboard at /admin/login.
| Field | Value |
|---|---|
| Username | admin |
| Password | admin |
Change these immediately after first login via Account Security in the dashboard.
