Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 216 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,216 @@
# interview-challenge
Technical challenge for LCSC tech team exec interviews.

## Solving instructions
- Create a fork of this repo and put all your changes in that.
- The problem can be solved in any stack, but the ones mentioned are better regarded.
- You're allowed to use AI but only for assistance.
- Challenge instructions are in `challenge.md`


## Movie Watchlist Application - README

### Table of Contents

1. [Project Overview](#project-overview)
2. [Core Features](#core-features)
3. [Technology Stack](#technology-stack)
4. [Setup Instructions](#setup-instructions)
- [Backend Setup](#backend-setup)
- [Frontend Setup](#frontend-setup)
- [Seeding the Database](#seeding-the-database)
5. [Working Demo](#working-demo)
6. [Sample Dataset](#sample-dataset)

---

### Project Overview

This is a full-stack web application that allows users to manage a movie watchlist. Users can add, update, search, and filter movies, mark movies as watched or unwatched, and rate them.

### Core Features

- **Movie List View**: Displays a list of movies with filters, search, and pagination.
- **Movie Details View**: Shows detailed information about each movie, allowing users to mark them as watched/unwatched and rate them.
- **Add Movie Form**: Users can add movies with a title, release year, and multiple genres.
- **Filters and Search**: Users can filter movies by genre, watch status, and rating, and perform a text search on movie titles.

---

### Technology Stack

#### Backend

- **Node.js** with **Express**: RESTful API for managing movies and genres.
- **MongoDB**: Flexible NoSQL database for scalability.
- **Mongoose**: MongoDB ODM for database interactions.
- **Express-Async-Handler**: For managing asynchronous routes.

#### Frontend

- **React**: Component-based architecture for UI.
- **TailwindCSS**: Utility-first CSS framework.
- **Axios**: For making HTTP requests.
- **React Router**: For handling routes.

#### Rationale for Technology Choices

- **Node.js + Express** is used for a lightweight, scalable backend.
- **MongoDB** is ideal for handling unstructured data like movie details and genres.
- **React** offers flexibility with a component-based structure for the frontend.

---

### Setup Instructions

#### Backend Setup

1. **Clone the repository:**

```bash
git clone https://github.com/your-username/movie-watchlist-backend.git
cd movie-watchlist-backend
```

2. **Install dependencies:**

```bash
npm install
```

3. **Set up environment variables:**
Create a `.env` file in the root directory and add the following values:

```
MONGO_URI=<your-mongodb-connection-string>
PORT=8000
```

4. **Run the MongoDB server:**
If you're using MongoDB locally, make sure the MongoDB server is running:

```bash
mongod
```

5. **Run the backend server:**

```bash
npm run dev - development
npm start - production
```

#### Frontend Setup

1. **Clone the repository:**

```bash
git clone https://github.com/your-username/movie-watchlist-frontend.git
cd movie-watchlist-frontend
```

2. **Install dependencies:**

```bash
npm install
```

3. **Set up environment variables:**
Create a `.env` file in the root directory and add:

```
REACT_APP_API_URL=http://localhost:8000/api/v1
```

4. **Run the frontend app:**

```bash
npm start
```

5. **Access the app:**
Navigate to `http://localhost:3000` in your browser to view the app.

---

### Seeding the Database

Before starting the application for the first time, you need to populate the database with some sample genres and movies. You can do this using the seed script.

1. **Run the seed script:**

After setting up the backend, run the following command in the `movie-watchlist-backend` directory:

```bash
npm run seed
```

This will populate the MongoDB database with a set of predefined genres and sample movies. You only need to run this command **once** unless you wish to reset the data.

---

### Working Demo

#### Local Setup Instructions

1. Ensure that **MongoDB** is running locally or provide a connection string to a hosted MongoDB instance (e.g., MongoDB Atlas).
2. Start the backend server first (`npm run dev` in `movie-watchlist-backend` directory).
3. Start the frontend server (`npm start` in `movie-watchlist-frontend` directory).
4. Open the frontend in your browser at `http://localhost:3000`.

#### API Endpoints

- `GET /api/v1/movies`: List movies with filters (e.g., genre, rating, watched status, search).
- `GET /api/v1/movies/:id`: Get details for a specific movie.
- `POST /api/v1/movies`: Add a new movie.
- `PATCH /api/v1/movies/:id`: Update movie details (mark as watched/unwatched, rate).
- `DELETE /api/v1/movies/:id`: Remove a movie.
- `GET /api/v1/genres`: List all available genres.

---

### Sample Dataset

The following data will be inserted into MongoDB when you run the seed script:

```json
[
{
"title": "Inception",
"releaseYear": 2010,
"genres": ["Action", "Sci-Fi"],
"watched": true,
"rating": 5
},
{
"title": "The Godfather",
"releaseYear": 1972,
"genres": ["Drama", "Crime"],
"watched": true,
"rating": 5
},
{
"title": "The Dark Knight",
"releaseYear": 2008,
"genres": ["Action", "Drama"],
"watched": true,
"rating": 4
},
{
"title": "Pulp Fiction",
"releaseYear": 1994,
"genres": ["Crime", "Drama"],
"watched": false,
"rating": null
},
{
"title": "Interstellar",
"releaseYear": 2014,
"genres": ["Adventure", "Drama", "Sci-Fi"],
"watched": true,
"rating": 5
}
]
```

You can import this sample data into MongoDB manually or allow the seed script to handle it.

---

### Conclusion

This README provides a complete guide to setting up both the backend and frontend, running the seed script, and understanding the core features and technology stack used in this movie watchlist application.

---
2 changes: 2 additions & 0 deletions movie-watchlist-backend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PORT = 8000
MONGO_URI=mongodb+srv://demiladealuko111:QewgeSscXR0H8TDB@cluster0.i2yza.mongodb.net/movie-wishlist?retryWrites=true&w=majority&appName=Cluster0
1 change: 1 addition & 0 deletions movie-watchlist-backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
17 changes: 17 additions & 0 deletions movie-watchlist-backend/controllers/genreController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const asyncHandler = require("express-async-handler");
const genreSchema = require("../models/genreSchema");

// GET: List all genres
const getGenres = asyncHandler(async (req, res) => {
try {
const genres = await genreSchema.find();
if (genres.length === 0) {
return res.status(404).json({ message: "No genres found." });
}
res.status(200).json(genres);
} catch (error) {
res.status(500).json({ message: "Failed to fetch genres.", error });
}
});

module.exports = { getGenres };
138 changes: 138 additions & 0 deletions movie-watchlist-backend/controllers/movieController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const asyncHandler = require("express-async-handler");
const movieSchema = require("../models/movieSchema");
const mongoose = require("mongoose");
const { buildMovieFilter } = require("../utils/MovieFilter");

// Helper function to validate MongoDB ObjectId
const isValidObjectId = (id) => mongoose.Types.ObjectId.isValid(id);

// GET: List all movies with pagination and filters
const getAllMovies = asyncHandler(async (req, res) => {
const { page = 1, limit = 10 } = req.query;

const filter = buildMovieFilter(req.query); // Use the external filter function
const skip = (page - 1) * limit;

try {
const movies = await movieSchema
.find(filter)
.skip(skip)
.limit(Number(limit));
const total = await movieSchema.countDocuments(filter);

res.status(200).json({
total,
page: Number(page),
pages: Math.ceil(total / limit),
movies,
});
} catch (error) {
res.status(500).json({ message: "Failed to fetch movies.", error });
}
});

// GET: Get a specific movie by ID
const getMovieById = asyncHandler(async (req, res) => {
const { id } = req.params;

if (!isValidObjectId(id)) {
return res.status(400).json({ message: "Invalid movie ID format." });
}

try {
const movie = await movieSchema.findById(id);
if (!movie) {
return res.status(404).json({ message: "Movie not found." });
}
res.status(200).json(movie);
} catch (error) {
res.status(500).json({ message: "Failed to fetch movie details.", error });
}
});

// POST: Create a new movie
const createMovie = asyncHandler(async (req, res) => {
const { title, releaseYear, genres, watched = false, rating } = req.body;

// Validation
if (!title || !releaseYear || !genres || genres.length === 0) {
return res.status(400).json({
message: "Title, release year, and at least one genre are required.",
});
}

try {
const newMovie = new movieSchema({
title,
releaseYear,
genres,
watched,
rating: watched ? rating : null, // Set rating only if watched
});

const savedMovie = await newMovie.save();
res.status(201).json(savedMovie);
} catch (error) {
res.status(500).json({ message: "Failed to create movie.", error });
}
});

// PATCH: Update movie details by ID (partial update)
const updateMovie = asyncHandler(async (req, res) => {
const { id } = req.params;
const { watched, rating } = req.body;

if (!isValidObjectId(id)) {
return res.status(400).json({ message: "Invalid movie ID format." });
}

// Ensure rating is between 1 and 5 if watched is true
if (watched && (rating < 1 || rating > 5)) {
return res.status(400).json({ message: "Rating must be between 1 and 5." });
}

try {
const movie = await movieSchema.findById(id);
if (!movie) {
return res.status(404).json({ message: "Movie not found." });
}

// Update movie details
movie.watched = watched !== undefined ? watched : movie.watched;
movie.rating = watched ? rating || movie.rating : 1; // Default rating when unwatched to 1

const updatedMovie = await movie.save();
res.status(200).json(updatedMovie);
} catch (error) {
res.status(500).json({ message: "Failed to update movie.", error });
}
});

// DELETE: Remove a movie by ID
const deleteMovie = asyncHandler(async (req, res) => {
const { id } = req.params;

if (!isValidObjectId(id)) {
return res.status(400).json({ message: "Invalid movie ID format." });
}

try {
const movie = await movieSchema.findById(id);
if (!movie) {
return res.status(404).json({ message: "Movie not found." });
}

await movie.remove();
res.status(200).json({ message: "Movie removed successfully." });
} catch (error) {
res.status(500).json({ message: "Failed to remove movie.", error });
}
});

module.exports = {
getAllMovies,
getMovieById,
createMovie,
updateMovie,
deleteMovie,
};
Loading