"हर शहर की अपनी कहानी है — शहरनामा उसे सुनाता है।"
"Every city has a story — Shaharnaama brings it to life."
🌍 Discover cities. Share your stories. Connect with fellow explorers — through a live community feed, group chats, and real-time direct messaging.
- Overview
- Live Demo
- Key Features
- Tech Stack
- Architecture
- Project Structure
- Getting Started
- Contributors
- License
Shaharnaama is an AI-powered, full-stack web platform that lets explorers discover the stories, culture, and art of cities — and connect with a global community of fellow travellers.
At its heart, Shaharnaama is a social platform for city lovers. Users share travel stories through a community feed, chat in real time — one-on-one or inside group rooms — and build connections over shared experiences. Every city page becomes a meeting point: explore, discuss, quiz each other, and climb a shared leaderboard together.
On top of the social layer, the platform integrates Google Gemini 2.5 Flash for intelligent city content generation, multi-API interactive maps (Mapbox GL, Leaflet, Google Maps) with a Virtual Walk mode, JWT + Google OAuth 2.0 authentication, and a fully gamified points and badge system — making it a production-grade, feature-rich application.
https://odyssey-qkji.onrender.com/Hero
⏱️ Hosted on Render's free tier — may take 30–60 seconds to spin up on first load.
The social backbone of Shaharnaama. Every explorer has a voice.
- Users can create posts with images, attach them to a city, and share experiences with the entire community — images are uploaded directly to Cloudinary via Multer.
- Posts support nested comments with replies, likes, bookmarks, and deletion — all JWT-protected, ensuring only authenticated users can interact.
- The feed is sorted by recency and enriched with relative timestamps, author avatars, city tags, and live like / comment counts.
- A sidebar panel surfaces live community stats: total posts, active members, total likes, trending cities, and latest activity — giving every page visit a sense of a living, active community.
- Every post and comment earns the author community points, which feed into the per-city leaderboard — making participation feel rewarding, not just social.
Built for genuine connection between explorers.
- Direct one-to-one messaging — users can privately message any other member. Conversations are persisted in MongoDB and loaded on demand, so history is never lost between sessions.
- Group chat rooms — users can create or join named groups and chat together in real time. On connection, the Socket.io server automatically joins each user into all of their existing group rooms — no manual re-join needed.
- Live typing indicators — when a user is typing, their counterpart sees a real-time signal, making conversations feel alive and immediate.
- Online presence tracking via
userSocketMap— all clients receive a live broadcast whenever a user connects or disconnects. AlastSeentimestamp is written to MongoDB on every disconnect, enabling accurate "last active" status. - WebSocket connections are JWT-authenticated at the handshake level — the socket middleware decodes and verifies the token from the HTTP-only cookie before the connection is established, rejecting unauthenticated clients entirely.
- Architecture keeps concerns clean: the REST API handles message persistence, Socket.io handles real-time relay only.
Turns community participation into friendly competition.
-
Every meaningful action earns points: +10 per post, +10 per correct quiz answer, comment points tracked separately — all stored in a
CityPointscollection scoped per user per city. -
Leaderboard renders a top-3 podium with animated rank icons, and a detailed breakdown bar showing each user's quiz / comment / post point split. Fully sortable by total score, quiz performance, or post activity.
-
Six badge tiers awarded automatically by total point threshold:
Points Badge 0 👤 No Points Yet 1–19 🌱 Newcomer 20–49 🌿 Local Explorer 50–99 ⭐ City Adventurer 100–199 💎 Elite Traveller 200+ 🏆 Legend Explorer
Powered by Google Gemini 2.5 Flash — every city comes alive.
- Dynamically generates city descriptions, curated must-do activity lists, and 3 adaptive quiz questions per city on demand — no static content, everything is fresh and context-aware.
- Implements a 12-hour MongoDB cache layer (TTL-based) — if a city's AI data is fresh in the database, Gemini is never called, reducing API cost and response latency significantly.
- Gemini also serves as a geocoder fallback: converts arrays of city names to
lat/lngcoordinates when not found in the local cache, with results persisted for future requests. - Quiz answers are graded server-side (behind a JWT-protected route) — scores are calculated securely, and each correct answer earns +10 points added to the user's city leaderboard score.
Three mapping APIs, one seamless exploration experience.
- Mapbox GL powers an auto-rotating 3D globe on the homepage, plotting top cities as markers with live visit counts. Rotation pauses on user interaction and auto-resumes after 3 seconds.
- Virtual Walk mode — geocodes a city via Geoapify, fetches nearby POIs, builds a nearest-neighbour walking route through up to 10 stops. Each stop dynamically fetches a photo from the Unsplash API, and the camera flies in with pitch + bearing animation for a 3D immersive fly-through.
- Personal visited-cities heatmap — users mark cities as visited; the backend tracks unique visitor counts per city, and a Mapbox heatmap visualises the user's global journey.
- City coordinates are cached in MongoDB via Gemini geocoding — no repeat API calls for the same location.
Secure, flexible, and production-grade authentication.
- Passport.js orchestrates three independent strategies: Local (email + password via
passport-local-mongoose), Google OAuth 2.0 (passport-google-oauth20), and JWT (passport-jwt) with dual token extraction — HTTP-only cookie ANDAuthorization: Bearerheader. - Signup requires email OTP verification — a 6-digit code is sent via Nodemailer (Gmail SMTP) and stored in MongoDB with a 5-minute TTL expiry. The account is only created on successful verification.
- JWT is issued on login and stored as an
HttpOnly,Secure,SameSite=Nonecookie for secure cross-origin auth. Google OAuth flow also terminates with a cookie-set redirect back to the React frontend. - All sensitive backend routes use
passport.authenticate("jwt", { session: false }). The frontend protects pages with a<ProtectedRoute>component backed by a/statuscheck withCache-Control: no-store.
A full profile and stats hub for every user.
- Dashboard aggregates personal stats, quiz history on Chart.js line & bar charts, city-point breakdowns per location, and a visited-cities Mapbox map — all fetched in a single
Promise.allfor minimal load time. - Users can update their name, bio, and default city, and upload a profile photo streamed directly to Cloudinary via Multer memory storage — no temporary disk writes.
- Activity tab displays the user's own posts and comments with in-place deletion — removing a post also reverses its associated points from
CityPoints. - Dark / light mode toggle persisted in
localStoragevia React Context (ThemeContext), applied as a class on the document root for full Tailwind CSS dark mode compatibility.
| Technology | Version | Purpose |
|---|---|---|
| React | 19 | Core UI framework |
| React Router | v7 | Client-side routing |
| Tailwind CSS | 3.4 | Utility-first styling |
| shadcn/ui | latest | Accessible component library |
| DaisyUI | 5 | Tailwind component plugin |
| Framer Motion | 12 | Animations & page transitions |
| Lucide React | latest | Icon system |
| Axios | 1.12 | HTTP client |
| Socket.io-client | 4.8 | Real-time WebSocket client |
| Technology | Version | Purpose |
|---|---|---|
| Node.js | LTS | JavaScript runtime |
| Express.js | 4.21 | Web framework & REST API |
| Socket.io | 4.8 | Real-time bidirectional communication |
| Passport.js | 0.7 | Authentication middleware |
| passport-local | 1.0 | Local email/password strategy |
| passport-google-oauth20 | 2.0 | Google OAuth 2.0 strategy |
| passport-jwt | 4.0 | JWT verification strategy |
| jsonwebtoken | 9.0 | JWT signing & verification |
| bcrypt | 6.0 | Password hashing |
| Nodemailer | 8.0 | OTP email delivery via Gmail SMTP |
| Multer | 2.0 | Multipart file upload handling |
| Cloudinary | 1.41 | Cloud media storage & CDN |
| Technology | Purpose |
|---|---|
| MongoDB | Primary NoSQL database |
| Mongoose | ODM — schema definition, validation, queries |
| TTL Indexes | Auto-expiring OTP tokens & Gemini cache documents |
| API / Service | Purpose |
|---|---|
| Google Gemini 2.5 Flash | City content generation, quiz creation, geocoding fallback |
| Google Maps API | Places data & map tiles |
| Mapbox GL | 3D globe, virtual walk map, personal heatmap |
| Geoapify | City-name to coordinates geocoding |
| Unsplash API | Live place photos for Virtual Walk mode |
| Library | Purpose |
|---|---|
| react-map-gl / mapbox-gl | 3D Mapbox globe & virtual walk |
| React Leaflet / Leaflet | Lightweight city detail maps |
| @react-google-maps/api | Google Maps integration |
| geolib | Geospatial distance calculations |
| Library | Purpose |
|---|---|
| Chart.js + react-chartjs-2 | Admin dashboard line & bar charts |
| Recharts | Community stats & leaderboard visuals |
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT (React 19) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Pages │ │Components│ │ Contexts│ │ Socket.io │ │
│ │Hero │ │Map │ │UserCtx │ │ Client │ │
│ │Community │ │Leaderboard│ │ThemeCtx │ │(JWT via cookie)│ │
│ │ChatPage │ │VirtualWalk│ └──────────┘ └───────────────┘ │
│ │Admin │ │SoundBox │ │
│ └──────────┘ └──────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS / WSS
┌────────────────────────────▼────────────────────────────────────┐
│ SERVER (Node.js + Express) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐│
│ │ REST Routes │ │ Socket.io │ │ Passport.js ││
│ │ /api/users │ │ Server │ │ ├─ Local Strategy ││
│ │ /api/listing│ │ ├─ Direct │ │ ├─ Google OAuth 2.0 ││
│ │ /api/comm.. │ │ │ Messages │ │ └─ JWT Strategy ││
│ │ /api/quiz │ │ ├─ Groups │ └────────────────────────┘│
│ │ /api/gemini │ │ └─ Presence │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────────────────────┐ │
│ │ Controllers │ │
│ │ geminiController · Generateinfo · listings · groups │ │
│ │ messages · user · comments · QuizResult · generateSound│ │
│ └──────┬──────────────────────────────────────────────────┘ │
└─────────┼────────────────────────────────────────────────────────┘
│
┌─────────▼────────────────────────────────────────────────────────┐
│ External Services │
│ ┌────────────┐ ┌───────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ MongoDB │ │ Gemini │ │Cloudinary│ │ Mapbox / │ │
│ │ Atlas │ │ 2.5 Flash│ │ CDN │ │ Geoapify / │ │
│ │ │ │ │ │ │ │ Unsplash │ │
│ └────────────┘ └───────────┘ └──────────┘ └─────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
User requests city info
│
▼
Check MongoDB cache (CityData collection)
│
┌────┴──────────────────────────┐
│ Cache hit AND age < 12 hours? │
└──┬─────────────────────────┬──┘
│ YES │ NO
▼ ▼
Return cached data Call Gemini 2.5 Flash
│
▼
Parse JSON response
│
▼
Upsert into MongoDB cache
│
▼
Return to client
Client A sends message
│
▼
POST /api/messages/send/:id ──► Save to MongoDB
│
▼
Socket.io emit "send_message"
│
▼
Server looks up receiverSocketId
in userSocketMap
│
▼
io.to(socketId).emit("receive_message")
│
▼
Client B receives message instantly
ODA-main/
├── backend/
│ ├── app.js # Express app entry point
│ ├── bin/www # HTTP server bootstrap
│ ├── config/
│ │ ├── cloudinary.js # Cloudinary SDK config
│ │ ├── passport.js # JWT strategy setup
│ │ ├── socket.js # Socket.io server + JWT auth middleware
│ │ └── upload.js # Multer + Cloudinary storage engine
│ ├── controllers/
│ │ ├── Generateinfo.js # Gemini city info + 12hr cache
│ │ ├── geminiController.js # Gemini geocoding + DB cache
│ │ ├── generateSound.js # AI ambient sound generation
│ │ ├── groups.js # Group creation & messaging
│ │ ├── listings.js # City listings CRUD
│ │ ├── messages.js # Direct message logic
│ │ ├── comments.js # Comment system
│ │ ├── QuizResult.js # Quiz result retrieval
│ │ └── user.js # User auth & profile
│ ├── models/
│ │ ├── users.js # User schema (passport-local-mongoose)
│ │ ├── listings.js # City listing schema
│ │ ├── Post.js # Community post schema
│ │ ├── messages.js # Direct message schema
│ │ ├── Group.js # Group chat schema
│ │ ├── CityData.js # Gemini response cache (12hr TTL)
│ │ ├── CityPoints.js # Per-user per-city points tracker
│ │ ├── quizResult.js # Quiz attempt records
│ │ ├── otp.js # OTP with 5-minute TTL expiry
│ │ ├── comment.js # Comment + reply schema
│ │ ├── Places.js # POI data schema
│ │ ├── city.js # Geocoded city coordinate cache
│ │ ├── category.js # Listing category schema
│ │ └── quiz.js # Quiz schema
│ ├── routes/
│ │ ├── users.js # Auth, profile, visit tracking
│ │ ├── community.js # Posts, likes, comments, points
│ │ ├── listings.js # City listing endpoints
│ │ ├── message.route.js # Direct message routes
│ │ ├── group_route.js # Group management routes
│ │ ├── submitQuiz.js # Quiz submit + server-side scoring
│ │ ├── GenerateInfo.js # Gemini city info route
│ │ ├── geminiRoute.js # Gemini geocoding route
│ │ ├── cityPoints.js # Leaderboard data routes
│ │ ├── places.js # POI routes
│ │ ├── sendOTP.js # OTP dispatch
│ │ ├── upload.js # File upload route
│ │ ├── generateSound.js # Sound generation route
│ │ ├── QuizResult.js # Quiz history route
│ │ └── index.js # Root route
│ └── utils/
│ └── verifyEmail.js # Nodemailer OTP sender
│
└── frontend/
├── public/
│ └── index.html
└── src/
├── App.js # Router + layout shell
├── UserContext.js # Global authenticated user state
├── ThemeContext.js # Dark / light mode context
├── components/
│ ├── Header.js
│ ├── Footer.js
│ ├── Map.js # Auto-rotating Mapbox globe
│ ├── Leaderboard.js # Podium + badge tiers
│ ├── Timeline.js # City history timeline
│ ├── SoundBox.js # AI ambient soundscape player
│ ├── CityInfo.js # Gemini city info + quiz UI
│ ├── CreatePost.jsx # Community post creation modal
│ ├── UserVisitedMap.js # Personal visited-cities heatmap
│ ├── MessageInput.js # Chat input component
│ ├── MessageList.js # Chat message list component
│ ├── ProtectedRoute.js # Auth route guard
│ ├── ThemeToggle.js # Dark / light switch
│ ├── ScrollToTop.js
│ ├── Admin/
│ │ ├── AdminOverview.js
│ │ ├── AdminAnalytics.js
│ │ ├── AdminProfile.js
│ │ ├── AdminActivity.js
│ │ └── AdminShared.js
│ └── VirtualWalk/
│ ├── VirtualWalkMap.js # Mapbox 3D walk + Unsplash photos
│ └── InfoPanel.js
├── pages/
│ ├── Hero.js # Landing / city explore page
│ ├── Specific.js # Individual city detail page
│ ├── Community.jsx # Social feed + sidebar stats
│ ├── ChatPage.jsx # Real-time DM + group chat UI
│ ├── Admin.js # Personal dashboard & analytics
│ ├── Explore.js # City browsing page
│ ├── LoginPage.js
│ ├── SignupPage.js
│ └── Check.js
└── utils/
├── config.js # BACKEND_URL config
└── socket.js # Socket.io client singleton
- Node.js v18 or higher
- npm v9 or higher
- MongoDB instance (local or MongoDB Atlas)
- API keys for: Gemini, Cloudinary, Mapbox, Google Maps, Geoapify, Unsplash
git clone https://github.com/lucky1426shrma/ODA.git
cd ODA# Backend
cd backend && npm install
# Frontend
cd ../frontend && npm installCreate backend/.env and frontend/.env with your API keys and secrets (MongoDB URI, JWT secret, Gemini key, Cloudinary credentials, Google OAuth credentials, Nodemailer config, Mapbox token, Geoapify key, Unsplash key).
# Backend (from /backend)
npm start
# Frontend (from /frontend)
npm startFrontend → http://localhost:3000 · Backend → http://localhost:5000
![]() Lucky Sharma |
![]() Arpit Goyal |
This project is licensed under the MIT License — see the LICENSE file for details.
Made with ❤️ by Lucky Sharma & Arpit Goyal

