A peer-to-peer time-slot scheduling application where users can swap their busy calendar slots with other users. Built with React, Node.js, Express, and MongoDB.
- Overview
- Design Choices
- Features
- Tech Stack
- Prerequisites
- Setup Instructions
- API Documentation
- Project Structure
- Assumptions & Challenges
- Docker Deployment
- License
SlotSwapper is a full-stack web application that enables users to manage their calendars and swap time slots with other users. The core concept is simple: users can mark their busy slots as "swappable," and other users can request to swap their own swappable slots for them. This creates a peer-to-peer marketplace for time slot exchanges.
- Calendar Management: Users can create, update, and delete calendar events
- Slot Swapping: Mark events as swappable and browse available slots from other users
- Swap Requests: Send and receive swap requests with accept/reject functionality
- Calendar Import: Import events from external calendar applications (iCal/ICS or CSV)
-
MongoDB with Mongoose: Chose MongoDB for its flexibility with document-based storage, which suits the dynamic nature of calendar events and swap requests. Mongoose provides schema validation and type safety.
-
TypeScript: Used TypeScript throughout the backend for type safety, better IDE support, and improved maintainability.
-
JWT Authentication: Implemented stateless JWT-based authentication for scalability and simplicity. Tokens expire after 7 days for security.
-
RESTful API Design: Followed REST principles with clear endpoint naming and HTTP method usage.
-
Zod Validation: Used Zod for runtime schema validation to ensure data integrity and provide clear error messages.
-
File Upload Handling: Used Multer for handling calendar file imports (ICS/iCal and CSV) with memory storage for efficiency.
-
React with TypeScript: Leveraged React for component-based UI development and TypeScript for type safety across the frontend.
-
Vite: Used Vite as the build tool for fast development experience and optimized production builds.
-
React Router: Implemented client-side routing for a single-page application experience.
-
Context API: Used React Context for global state management (authentication and theme).
-
Axios: Used Axios for API calls with interceptors to automatically attach JWT tokens.
-
Responsive Design: Implemented a mobile-first responsive design with hamburger menu for mobile navigation.
-
Dark Mode: Built-in dark/light mode toggle with system preference detection and localStorage persistence.
-
Modern Aesthetic: Created a vibrant, engaging UI with gradients, glassmorphism effects, and smooth animations.
-
User-Friendly: Clear visual hierarchy, intuitive navigation, and immediate feedback for user actions.
-
Accessibility: Ensured proper contrast ratios, readable fonts, and keyboard navigation support.
-
Performance: Optimized images, lazy loading, and efficient state management for fast load times.
- ✅ User Authentication: Secure sign up and login with JWT tokens
- ✅ Calendar Management: Full CRUD operations for events
- ✅ Event Status System: Events can be BUSY, SWAPPABLE, or SWAP_PENDING
- ✅ Swap Marketplace: Browse available swappable slots from other users
- ✅ Swap Requests: Create, accept, and reject swap requests
- ✅ Calendar Import: Import events from iCal/ICS or CSV files
- ✅ Real-time Updates: Automatic calendar refresh when swaps are accepted
- ✅ Dark Mode: Toggle between light and dark themes
- ✅ Responsive Design: Works seamlessly on desktop, tablet, and mobile devices
- ✅ Auto-refresh: Marketplace and notifications update automatically
- Runtime: Node.js 20
- Framework: Express.js
- Language: TypeScript
- Database: MongoDB with Mongoose ODM
- Authentication: JWT (jsonwebtoken)
- Password Hashing: bcryptjs
- Validation: Zod
- File Upload: Multer
- Calendar Parsing: ical.js
- CSV Parsing: csv-parser
- Framework: React 18
- Language: TypeScript
- Build Tool: Vite
- Routing: React Router v6
- HTTP Client: Axios
- Styling: CSS with custom properties
- Fonts: Google Fonts (Inter, Space Grotesk)
Before you begin, ensure you have the following installed:
- Node.js: v18 or higher (Download)
- npm: v9 or higher (comes with Node.js)
- MongoDB:
git clone <repository-url>
cd Slot-Swapper-
Navigate to the backend directory:
cd backend -
Install dependencies:
npm install
-
Create a
.envfile in thebackenddirectory:DATABASE_URL=mongodb://localhost:27017/slotswapper JWT_SECRET=your-super-secret-jwt-key-change-this-in-production PORT=3001
For MongoDB Atlas, use your connection string:
DATABASE_URL=mongodb+srv://username:password@cluster.mongodb.net/slotswapper?retryWrites=true&w=majority
Important: The database name (
slotswapper) must be included in the connection string! -
Start the development server:
npm run dev
The backend server will start on
http://localhost:3001
-
Open a new terminal and navigate to the frontend directory:
cd frontend -
Install dependencies:
npm install
-
Start the development server:
npm run dev
The frontend will start on
http://localhost:3000
- Open your browser and navigate to
http://localhost:3000 - You should see the login page
- Create a new account or log in with existing credentials
- The application should be fully functional!
http://localhost:3001/api
All endpoints except /auth/signup and /auth/login require authentication. Include the JWT token in the Authorization header:
Authorization: Bearer <your-jwt-token>
Create a new user account.
Request Body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123"
}Response (201):
{
"user": {
"id": "507f1f77bcf86cd799439011",
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-01-01T00:00:00.000Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Error Responses:
400: User already exists or validation error500: Server error
Login and receive JWT token.
Request Body:
{
"email": "john@example.com",
"password": "password123"
}Response (200):
{
"user": {
"id": "507f1f77bcf86cd799439011",
"name": "John Doe",
"email": "john@example.com"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Error Responses:
401: Invalid credentials500: Server error
Get all events for the authenticated user.
Headers:
Authorization: Bearer <token>
Response (200):
[
{
"id": "507f1f77bcf86cd799439011",
"title": "Team Meeting",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z",
"status": "BUSY",
"userId": "507f1f77bcf86cd799439012"
}
]Get a specific event by ID.
Headers:
Authorization: Bearer <token>
Response (200):
{
"id": "507f1f77bcf86cd799439011",
"title": "Team Meeting",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z",
"status": "BUSY",
"userId": "507f1f77bcf86cd799439012"
}Error Responses:
404: Event not found403: Event does not belong to user
Create a new event.
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Request Body:
{
"title": "Team Meeting",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z",
"status": "BUSY"
}Note: status is optional and defaults to "BUSY". Valid values: "BUSY", "SWAPPABLE", "SWAP_PENDING".
Response (201):
{
"id": "507f1f77bcf86cd799439011",
"title": "Team Meeting",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z",
"status": "BUSY",
"userId": "507f1f77bcf86cd799439012"
}Error Responses:
400: Validation error500: Server error
Update an existing event.
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Request Body (all fields optional):
{
"title": "Updated Meeting",
"startTime": "2024-01-15T11:00:00.000Z",
"endTime": "2024-01-15T12:00:00.000Z",
"status": "SWAPPABLE"
}Response (200):
{
"id": "507f1f77bcf86cd799439011",
"title": "Updated Meeting",
"startTime": "2024-01-15T11:00:00.000Z",
"endTime": "2024-01-15T12:00:00.000Z",
"status": "SWAPPABLE",
"userId": "507f1f77bcf86cd799439012"
}Error Responses:
400: Validation error403: Event does not belong to user404: Event not found
Delete an event.
Headers:
Authorization: Bearer <token>
Response (200):
{
"message": "Event deleted successfully"
}Error Responses:
403: Event does not belong to user404: Event not found
Get all swappable slots from other users (excludes current user's slots).
Headers:
Authorization: Bearer <token>
Response (200):
[
{
"id": "507f1f77bcf86cd799439011",
"title": "Available Slot",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z",
"status": "SWAPPABLE",
"userId": "507f1f77bcf86cd799439012",
"user": {
"id": "507f1f77bcf86cd799439012",
"name": "Jane Doe",
"email": "jane@example.com"
}
}
]Create a swap request.
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Request Body:
{
"mySlotId": "507f1f77bcf86cd799439013",
"theirSlotId": "507f1f77bcf86cd799439011"
}Response (201):
{
"id": "507f1f77bcf86cd799439014",
"requesterId": "507f1f77bcf86cd799439012",
"requestedId": "507f1f77bcf86cd799439015",
"requesterSlotId": "507f1f77bcf86cd799439013",
"requestedSlotId": "507f1f77bcf86cd799439011",
"status": "PENDING",
"requester": {
"id": "507f1f77bcf86cd799439012",
"name": "John Doe",
"email": "john@example.com"
},
"requested": {
"id": "507f1f77bcf86cd799439015",
"name": "Jane Doe",
"email": "jane@example.com"
},
"requesterSlot": {
"id": "507f1f77bcf86cd799439013",
"title": "My Available Slot",
"startTime": "2024-01-16T10:00:00.000Z",
"endTime": "2024-01-16T11:00:00.000Z"
},
"requestedSlot": {
"id": "507f1f77bcf86cd799439011",
"title": "Their Available Slot",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z"
}
}Error Responses:
400: Invalid slot IDs or validation error404: One or both slots not found403: One or both slots don't belong to the correct users409: Slot already has a pending swap request
Accept or reject a swap request.
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Request Body:
{
"accepted": true
}Response (200):
{
"message": "Swap request accepted",
"swapRequest": {
"id": "507f1f77bcf86cd799439014",
"status": "ACCEPTED",
...
}
}Note: When a swap is accepted:
- Both slots' statuses are updated to
SWAP_PENDING - The requester's slot's
startTimeandendTimeare swapped with the requested slot's times - The requested user's slot's
startTimeandendTimeare swapped with the requester's slot's times
Error Responses:
400: Invalid request body403: User is not the requested user404: Swap request not found409: Swap request already processed
Get all swap requests (incoming and outgoing) for the authenticated user.
Headers:
Authorization: Bearer <token>
Response (200):
{
"incoming": [
{
"id": "507f1f77bcf86cd799439014",
"status": "PENDING",
"requester": {
"id": "507f1f77bcf86cd799439012",
"name": "John Doe",
"email": "john@example.com"
},
"requesterSlot": {
"id": "507f1f77bcf86cd799439013",
"title": "John's Slot",
"startTime": "2024-01-16T10:00:00.000Z",
"endTime": "2024-01-16T11:00:00.000Z"
},
"requestedSlot": {
"id": "507f1f77bcf86cd799439011",
"title": "My Slot",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z"
}
}
],
"outgoing": [
{
"id": "507f1f77bcf86cd799439015",
"status": "PENDING",
"requested": {
"id": "507f1f77bcf86cd799439016",
"name": "Jane Doe",
"email": "jane@example.com"
},
"requesterSlot": {
"id": "507f1f77bcf86cd799439017",
"title": "My Slot",
"startTime": "2024-01-16T10:00:00.000Z",
"endTime": "2024-01-16T11:00:00.000Z"
},
"requestedSlot": {
"id": "507f1f77bcf86cd799439018",
"title": "Jane's Slot",
"startTime": "2024-01-15T10:00:00.000Z",
"endTime": "2024-01-15T11:00:00.000Z"
}
}
]
}Import calendar events from a file (ICS/iCal or CSV).
Headers:
Authorization: Bearer <token>
Content-Type: multipart/form-data
Request Body:
- Form field:
file(file upload) - Accepted formats:
.ics,.ical,.csv - Maximum file size: 10MB
Response (200):
{
"message": "Calendar imported successfully",
"imported": 5,
"skipped": 2
}Error Responses:
400: Invalid file type or no file provided413: File too large500: Server error during parsing
Note:
- Past events (more than 1 hour ago) are automatically filtered out
- Duplicate events (same start time within 1 minute) are skipped
- Invalid date formats are skipped
Check if the server is running.
Response (200):
{
"status": "ok"
}Slot-Swapper/
├── backend/
│ ├── src/
│ │ ├── models/ # Mongoose models (User, Event, SwapRequest)
│ │ ├── routes/ # API route handlers
│ │ │ ├── auth.ts # Authentication endpoints
│ │ │ ├── events.ts # Event CRUD endpoints
│ │ │ ├── swaps.ts # Swap logic endpoints
│ │ │ └── import.ts # Calendar import endpoint
│ │ ├── middleware/ # Express middleware
│ │ │ └── auth.ts # JWT authentication middleware
│ │ └── server.ts # Express server setup
│ ├── package.json
│ ├── tsconfig.json
│ └── README.md
├── frontend/
│ ├── src/
│ │ ├── components/ # Reusable React components
│ │ │ ├── Layout.tsx # Main layout with navigation
│ │ │ └── ThemeToggle.tsx # Dark/light mode toggle
│ │ ├── pages/ # Page components
│ │ │ ├── Login.tsx # Login page
│ │ │ ├── Signup.tsx # Signup page
│ │ │ ├── Dashboard.tsx # Calendar management page
│ │ │ ├── Marketplace.tsx # Browse swappable slots
│ │ │ └── Notifications.tsx # Swap requests page
│ │ ├── contexts/ # React contexts
│ │ │ ├── AuthContext.tsx # Authentication context
│ │ │ └── ThemeContext.tsx # Theme context
│ │ ├── services/ # API service functions
│ │ │ └── api.ts # Axios API client
│ │ ├── App.tsx # Main app component
│ │ └── main.tsx # Entry point
│ ├── package.json
│ ├── vite.config.ts
│ └── tsconfig.json
├── Dockerfile # Unified Dockerfile for both frontend and backend
├── .dockerignore # Docker ignore file
├── docker-compose.yml # Docker Compose configuration
├── README.DOCKER.md # Docker deployment guide
└── README.md # This file
-
User Behavior:
- Users understand that swapping slots means exchanging time periods
- Users will mark slots as swappable before browsing the marketplace
- Users can only have one pending swap request per slot
-
Data Integrity:
- MongoDB connection is reliable and persistent
- JWT tokens are stored securely in localStorage (client-side)
- File uploads are temporary and don't require persistent storage
-
Time Handling:
- All times are stored in UTC and converted to user's local timezone in the frontend
- Past events (1+ hour ago) are filtered out during import
- Events with duplicate start times (within 1 minute) are considered duplicates
-
Scalability:
- Application is designed for moderate user loads
- MongoDB Atlas is used for production deployments
- No real-time features (polling is used for updates)
-
Database Migration:
- Challenge: Migrated from PostgreSQL with Prisma to MongoDB with Mongoose
- Solution: Rewrote all models and queries to use Mongoose, updated type definitions, and ensured proper ObjectId handling
-
Swap Logic Complexity:
- Challenge: Implementing atomic swap operations where both slots' times are exchanged
- Solution: Used Mongoose transactions to ensure data consistency and prevent race conditions
-
Calendar Import:
- Challenge: Parsing different calendar formats (ICS/iCal and CSV) with varying date formats
- Solution: Used ical.js for ICS parsing, implemented custom CSV parser with robust error handling, added timezone buffer for past event filtering
-
UI/UX Responsiveness:
- Challenge: Making the application work seamlessly on mobile devices with limited screen space
- Solution: Implemented responsive design with media queries, hamburger menu for mobile navigation, and optimized spacing for smaller screens
-
State Management:
- Challenge: Keeping frontend state synchronized with backend data (especially after swaps)
- Solution: Implemented auto-refresh mechanisms using useEffect hooks, window focus listeners, and explicit refresh buttons
-
Dark Mode Implementation:
- Challenge: Ensuring consistent styling across all pages in both light and dark modes
- Solution: Created comprehensive dark mode styles for all components, used CSS custom properties where possible, and ensured proper contrast ratios
-
Type Safety:
- Challenge: Maintaining type safety between frontend and backend with different data formats (ObjectId vs string)
- Solution: Used TypeScript interfaces consistently, added proper type transformations in API responses, and used Zod for runtime validation
-
File Upload Handling:
- Challenge: Handling large calendar files and parsing errors gracefully
- Solution: Implemented file size limits (10MB), added try-catch blocks around individual event parsing, and provided clear error messages
See README.DOCKER.md for detailed Docker deployment instructions.
The project includes a single unified Dockerfile in the root directory that builds both frontend and backend services using multi-stage builds:
# Create .env file in root directory
DATABASE_URL=mongodb+srv://username:password@cluster.mongodb.net/slotswapper
JWT_SECRET=your-secret-key
# Build and run both services
docker-compose up -d --buildThe root Dockerfile uses multi-stage builds with four stages:
- backend-builder: Compiles TypeScript backend code
- backend: Production backend image (Node.js)
- frontend-builder: Builds React frontend with Vite
- frontend: Production frontend image (Nginx)
Docker Compose uses build targets (backend and frontend) to create separate containers from the same Dockerfile.
- Frontend: http://localhost
- Backend API: http://localhost:3001
# Build backend only
docker build --target backend -t slotswapper-backend .
# Build frontend only
docker build --target frontend -t slotswapper-frontend .cd backend
# Development (with hot reload)
npm run dev
# Build for production
npm run build
# Start production server
npm startcd frontend
# Development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run previewSign Up:
curl -X POST http://localhost:3001/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com","password":"password123"}'Login:
curl -X POST http://localhost:3001/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","password":"password123"}'Get Events (with token):
curl -X GET http://localhost:3001/api/events \
-H "Authorization: Bearer YOUR_TOKEN_HERE"- Import the API endpoints into Postman
- Set up environment variables:
base_url:http://localhost:3001/apitoken: (will be set after login)
- Create requests for each endpoint
- Use the "Tests" tab to automatically save the token after login
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
For issues or questions, please open an issue on the GitHub repository.
Built with ❤️ using React, Node.js, Express, and MongoDB