├── MovieAPI ├── .dockerignore ├── .gitignore ├── Controllers │ └── MoviesController.cs ├── Data │ ├── MovieContext.cs │ └── Seeding │ │ └── MovieSeedData.cs ├── Dockerfile ├── Extensions │ └── SwaggerServiceExtensions.cs ├── Migrations │ ├── 20240815043414_InitialCreate.Designer.cs │ ├── 20240815043414_InitialCreate.cs │ ├── 20240815061531_AddGenresToMovie.Designer.cs │ ├── 20240815061531_AddGenresToMovie.cs │ ├── 20240815101318_SeedInitialMovies.Designer.cs │ ├── 20240815101318_SeedInitialMovies.cs │ ├── 20240816111759_RemoveImageUrl.Designer.cs │ ├── 20240816111759_RemoveImageUrl.cs │ └── MovieContextModelSnapshot.cs ├── Models │ ├── Movie.cs │ └── PaginatedResponse.cs ├── MovieAPI.csproj ├── MovieAPI.generated.sln ├── MovieAPI.http ├── Program.cs ├── Properties │ └── launchSettings.json ├── README.md ├── Repositories │ ├── IMovieRepository.cs │ └── MovieRepository.cs ├── Services │ ├── IMovieService.cs │ └── MovieService.cs ├── appsettings.Development.json ├── docker-compose.yml └── launchSettings.json ├── MovieAppUI ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── public │ ├── assets │ │ ├── default.png │ │ ├── event.jpg │ │ └── logo.jpg │ └── favicon.ico ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── components │ │ │ ├── confirm-dialog │ │ │ │ ├── confirm-dialog.component.html │ │ │ │ ├── confirm-dialog.component.scss │ │ │ │ ├── confirm-dialog.component.spec.ts │ │ │ │ └── confirm-dialog.component.ts │ │ │ ├── movie-card │ │ │ │ ├── movie-card.component.html │ │ │ │ ├── movie-card.component.scss │ │ │ │ ├── movie-card.component.spec.ts │ │ │ │ └── movie-card.component.ts │ │ │ └── skeleton-movie-card │ │ │ │ ├── skeleton-movie-card.component.html │ │ │ │ ├── skeleton-movie-card.component.scss │ │ │ │ ├── skeleton-movie-card.component.spec.ts │ │ │ │ └── skeleton-movie-card.component.ts │ │ ├── pages │ │ │ ├── create-movie │ │ │ │ ├── create-movie.component.html │ │ │ │ ├── create-movie.component.scss │ │ │ │ ├── create-movie.component.spec.ts │ │ │ │ └── create-movie.component.ts │ │ │ ├── movie-detail │ │ │ │ ├── movie-detail.component.html │ │ │ │ ├── movie-detail.component.scss │ │ │ │ ├── movie-detail.component.spec.ts │ │ │ │ └── movie-detail.component.ts │ │ │ └── movies-list │ │ │ │ ├── movies-list.component.html │ │ │ │ ├── movies-list.component.scss │ │ │ │ ├── movies-list.component.spec.ts │ │ │ │ └── movies-list.component.ts │ │ └── services │ │ │ ├── movie.service.spec.ts │ │ │ └── movie.service.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── index.html │ ├── main.ts │ └── styles.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json └── README.md /MovieAPI/.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | **/*.user 4 | **/*.vspscc 5 | **/*.vssscc 6 | **/*.suo 7 | -------------------------------------------------------------------------------- /MovieAPI/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Visual Studio temporary files, build results, and 2 | # Include Rider, VS Code, and other typical IDE settings 3 | 4 | # User-specific files 5 | *.rsuser 6 | *.suo 7 | *.user 8 | *.userosscache 9 | *.sln.docstates 10 | 11 | # User-specific files (Mono Auto Generated) 12 | mono_crash.* 13 | 14 | # User-specific files (Visual Studio Code) 15 | .vscode/ 16 | 17 | # User-specific files (Rider) 18 | .idea/ 19 | 20 | # Auto-generated files 21 | *.log 22 | *.tlog 23 | *.vspscc 24 | *.vssscc 25 | 26 | # Ignore the build output directories 27 | bin/ 28 | obj/ 29 | 30 | # ASP.NET Scaffolding 31 | ScaffoldingReadMe.txt 32 | 33 | # Uncomment if you have tasks that create the following file and uncomment if you have tasks that create the following file 34 | # /tools/SignTool 35 | 36 | # Visual Studio 2019/2022 37 | .vs/ 38 | 39 | # Local History for JetBrains IDEs 40 | .idea/.idea.Directory 41 | 42 | # Windows image file caches 43 | Thumbs.db 44 | ehthumbs.db 45 | 46 | # Folder config file 47 | Desktop.ini 48 | 49 | # Recycle Bin used on file shares 50 | $RECYCLE.BIN/ 51 | 52 | # Windows Installer files 53 | *.cab 54 | *.msi 55 | *.msm 56 | *.msp 57 | 58 | # Windows shortcuts 59 | *.lnk 60 | 61 | # JetBrains Rider 62 | .idea/ 63 | 64 | # Rider settings folder 65 | .idea/ 66 | 67 | # Rider Project files 68 | *.sln.iml 69 | 70 | # Rider/Resharper comment files 71 | *.DotSettings.user 72 | 73 | # Rider logs 74 | logs/ 75 | 76 | # Exclude database files 77 | *.mdf 78 | *.ldf 79 | *.ndf 80 | 81 | # Visual Studio Code workspace settings 82 | .vscode/ 83 | 84 | # Rider Plugin files 85 | .idea/**/plugins/ 86 | 87 | # Rider IML files 88 | *.iml 89 | 90 | # Ignore files generated by popular CI/CD tools 91 | !.github/ 92 | !.gitlab/ 93 | 94 | # Other files and folders 95 | .DS_Store 96 | npm-debug.log 97 | yarn-debug.log 98 | yarn-error.log 99 | 100 | # Ignore wwwroot static files if necessary 101 | wwwroot/images/* 102 | !wwwroot/images/.gitkeep 103 | 104 | # Ignore coverage reports 105 | coverage/ 106 | 107 | # Secrets and configurations 108 | appsettings*.json 109 | !appsettings.Development.json -------------------------------------------------------------------------------- /MovieAPI/Controllers/MoviesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using MovieAPI.Models; 4 | using MovieAPI.Services; 5 | using Swashbuckle.AspNetCore.Annotations; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace MovieAPI.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | [ApiController] 14 | public class MoviesController : ControllerBase 15 | { 16 | private readonly IMovieService _movieService; 17 | 18 | public MoviesController(IMovieService movieService) 19 | { 20 | _movieService = movieService; 21 | } 22 | 23 | [HttpGet] 24 | [SwaggerOperation(Summary = "Retrieves a paginated list of all movies.")] 25 | [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved movies.", typeof(PaginatedResponse))] 26 | public async Task>> GetMovies( 27 | [FromQuery] int pageNumber = 1, 28 | [FromQuery] int pageSize = 10) 29 | { 30 | var paginatedMovies = await _movieService.GetAllMoviesAsync(pageNumber, pageSize); 31 | return Ok(paginatedMovies); 32 | } 33 | 34 | [HttpGet("{id}")] 35 | [SwaggerOperation(Summary = "Retrieves a specific movie by its ID.", Description = "Returns the requested movie.")] 36 | [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved movie.", typeof(Movie))] 37 | [SwaggerResponse(StatusCodes.Status404NotFound, "Movie not found.")] 38 | public async Task> GetMovie(int id) 39 | { 40 | var movie = await _movieService.GetMovieByIdAsync(id); 41 | 42 | if (movie == null) 43 | { 44 | return NotFound(); 45 | } 46 | 47 | return Ok(movie); 48 | } 49 | 50 | [HttpPost] 51 | [SwaggerOperation(Summary = "Creates a new movie.", Description = "Creates a new movie and returns the created movie.")] 52 | [SwaggerResponse(StatusCodes.Status201Created, "Successfully created movie.", typeof(Movie))] 53 | [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid input.")] 54 | public async Task> PostMovie([FromForm] Movie movie, [FromForm] IFormFile coverImage, [FromForm] string genre) 55 | { 56 | if (!string.IsNullOrWhiteSpace(genre)) 57 | { 58 | movie.Genre = genre.Split(',') 59 | .Select(g => g.Trim()) 60 | .ToList(); 61 | } 62 | 63 | var createdMovie = await _movieService.CreateMovieAsync(movie, coverImage); 64 | return CreatedAtAction(nameof(GetMovie), new { id = createdMovie.Id }, createdMovie); 65 | } 66 | 67 | [HttpPut("{id}")] 68 | [SwaggerOperation(Summary = "Updates an existing movie.", Description = "Updates the specified movie with new data.")] 69 | [SwaggerResponse(StatusCodes.Status204NoContent, "Successfully updated movie.")] 70 | [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid input.")] 71 | [SwaggerResponse(StatusCodes.Status404NotFound, "Movie not found.")] 72 | public async Task PutMovie(int id, [FromForm] Movie movie, [FromForm] IFormFile? coverImage, [FromForm] string genre) 73 | { 74 | Console.WriteLine($"Movie ID: {id}"); 75 | Console.WriteLine($"Title: {movie.Title}"); 76 | Console.WriteLine($"Description: {movie.Description}"); 77 | Console.WriteLine($"Genre: {string.Join(", ", movie.Genre)}"); 78 | Console.WriteLine($"CoverImage: {coverImage?.FileName ?? "No image uploaded"}"); 79 | Console.WriteLine($"Existing CoverImage Path: {movie.CoverImage}"); 80 | 81 | if (id != movie.Id) 82 | { 83 | return BadRequest(); 84 | } 85 | 86 | if (!string.IsNullOrWhiteSpace(genre)) 87 | { 88 | movie.Genre = genre.Split(',') 89 | .Select(g => g.Trim()) 90 | .ToList(); 91 | } 92 | 93 | try 94 | { 95 | await _movieService.UpdateMovieAsync(movie, coverImage); 96 | } 97 | catch (KeyNotFoundException) 98 | { 99 | return NotFound(); 100 | } 101 | 102 | return NoContent(); 103 | } 104 | 105 | [HttpDelete("{id}")] 106 | [SwaggerOperation(Summary = "Deletes a specific movie by its ID.", Description = "Deletes the specified movie.")] 107 | [SwaggerResponse(StatusCodes.Status204NoContent, "Successfully deleted movie.")] 108 | [SwaggerResponse(StatusCodes.Status404NotFound, "Movie not found.")] 109 | public async Task DeleteMovie(int id) 110 | { 111 | try 112 | { 113 | await _movieService.DeleteMovieAsync(id); 114 | } 115 | catch (KeyNotFoundException) 116 | { 117 | return NotFound(); 118 | } 119 | 120 | return NoContent(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /MovieAPI/Data/MovieContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using MovieAPI.Models; 3 | using MovieAPI.Data.Seeding; 4 | 5 | namespace MovieAPI.Data 6 | { 7 | public class MovieContext : DbContext 8 | { 9 | public MovieContext(DbContextOptions options) : base(options) 10 | { 11 | } 12 | 13 | public DbSet Movies { get; set; } 14 | 15 | protected override void OnModelCreating(ModelBuilder modelBuilder) 16 | { 17 | base.OnModelCreating(modelBuilder); 18 | 19 | // Configure Genre property conversion 20 | modelBuilder.Entity() 21 | .Property(m => m.Genre) 22 | .HasConversion( 23 | v => string.Join(',', v), // Convert List to string for storage 24 | v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()); // Convert string to List when retrieved 25 | 26 | // Apply the seed data 27 | MovieSeedData.Seed(modelBuilder); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MovieAPI/Data/Seeding/MovieSeedData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using MovieAPI.Models; 3 | 4 | namespace MovieAPI.Data.Seeding 5 | { 6 | public static class MovieSeedData 7 | { 8 | public static void Seed(ModelBuilder modelBuilder) 9 | { 10 | modelBuilder.Entity().HasData( 11 | new Movie 12 | { 13 | Id = 1, 14 | Title = "The Dark Knight", 15 | Description = "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.", 16 | Genre = new List { "Action", "Drama", "Thriller" }, 17 | CoverImage = "" 18 | }, 19 | new Movie 20 | { 21 | Id = 2, 22 | Title = "Inception", 23 | Description = "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.", 24 | Genre = new List { "Sci-Fi", "Action", "Thriller" }, 25 | CoverImage = "" 26 | }, 27 | new Movie 28 | { 29 | Id = 3, 30 | Title = "Forrest Gump", 31 | Description = "The presidencies of Kennedy and Johnson, the Vietnam War, the Watergate scandal and other historical events unfold from the perspective of an Alabama man with an IQ of 75, whose only desire is to be reunited with his childhood sweetheart.", 32 | Genre = new List { "Drama", "Romance" }, 33 | CoverImage = "" 34 | }, 35 | new Movie 36 | { 37 | Id = 4, 38 | Title = "Up", 39 | Description = "Seventy-eight year old Carl Fredricksen travels to Paradise Falls in his house equipped with balloons, inadvertently taking a young stowaway.", 40 | Genre = new List { "Animation", "Adventure", "Family" }, 41 | CoverImage = "" 42 | }, 43 | new Movie 44 | { 45 | Id = 5, 46 | Title = "The Shawshank Redemption", 47 | Description = "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", 48 | Genre = new List { "Drama" }, 49 | CoverImage = "" 50 | }, 51 | new Movie 52 | { 53 | Id = 6, 54 | Title = "The Godfather", 55 | Description = "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", 56 | Genre = new List { "Crime", "Drama" }, 57 | CoverImage = "" 58 | }, 59 | new Movie 60 | { 61 | Id = 7, 62 | Title = "The Godfather: Part II", 63 | Description = "The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.", 64 | Genre = new List { "Crime", "Drama" }, 65 | CoverImage = "" 66 | }, 67 | new Movie 68 | { 69 | Id = 8, 70 | Title = "The Dark Knight Rises", 71 | Description = "Eight years after the Joker's reign of anarchy, Batman is forced from his exile with the help of the enigmatic Selina Kyle to save Gotham City from the brutal guerrilla terrorist Bane.", 72 | Genre = new List { "Action", "Drama", "Thriller" }, 73 | CoverImage = "" 74 | }, 75 | new Movie 76 | { 77 | Id = 9, 78 | Title = "Interstellar", 79 | Description = "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", 80 | Genre = new List { "Adventure", "Drama", "Sci-Fi" }, 81 | CoverImage = "" 82 | }, 83 | new Movie 84 | { 85 | Id = 10, 86 | Title = "Gladiator", 87 | Description = "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family and sent him into slavery.", 88 | Genre = new List { "Action", "Adventure", "Drama" }, 89 | CoverImage = "" 90 | }, 91 | new Movie 92 | { 93 | Id = 11, 94 | Title = "Braveheart", 95 | Description = "Scottish warrior William Wallace leads his countrymen in a rebellion to free his homeland from the tyranny of King Edward I of England.", 96 | Genre = new List { "Biography", "Drama", "History" }, 97 | CoverImage = "" 98 | }, 99 | new Movie 100 | { 101 | Id = 12, 102 | Title = "Saving Private Ryan", 103 | Description = "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action.", 104 | Genre = new List { "Drama", "War" }, 105 | CoverImage = "" 106 | }, 107 | new Movie 108 | { 109 | Id = 13, 110 | Title = "Schindler's List", 111 | Description = "In German-occupied Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.", 112 | Genre = new List { "Biography", "Drama", "History" }, 113 | CoverImage = "" 114 | }, 115 | new Movie 116 | { 117 | Id = 14, 118 | Title = "The Silence of the Lambs", 119 | Description = "A young FBI cadet must receive the help of an incarcerated and manipulative cannibal killer to help catch another serial killer, a madman who skins his victims.", 120 | Genre = new List { "Crime", "Drama", "Thriller" }, 121 | CoverImage = "" 122 | }, 123 | new Movie 124 | { 125 | Id = 15, 126 | Title = "Fight Club", 127 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 128 | Genre = new List { "Drama" }, 129 | CoverImage = "" 130 | }, 131 | new Movie 132 | { 133 | Id = 16, 134 | Title = "The Matrix", 135 | Description = "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", 136 | Genre = new List { "Action", "Sci-Fi" }, 137 | CoverImage = "" 138 | }, 139 | new Movie 140 | { 141 | Id = 17, 142 | Title = "The Lord of the Rings: The Return of the King", 143 | Description = "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", 144 | Genre = new List { "Adventure", "Drama", "Fantasy" }, 145 | CoverImage = "" 146 | }, 147 | new Movie 148 | { 149 | Id = 18, 150 | Title = "The Lord of the Rings: The Two Towers", 151 | Description = "While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard.", 152 | Genre = new List { "Adventure", "Drama", "Fantasy" }, 153 | CoverImage = "" 154 | }, 155 | new Movie 156 | { 157 | Id = 19, 158 | Title = "The Lord of the Rings: The Fellowship of the Ring", 159 | Description = "A meek Hobbit from the Shire and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron.", 160 | Genre = new List { "Adventure", "Drama", "Fantasy" }, 161 | CoverImage = "" 162 | }, 163 | new Movie 164 | { 165 | Id = 20, 166 | Title = "Star Wars: Episode IV - A New Hope", 167 | Description = "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee, and two droids to save the galaxy from the Empire's world-destroying battle station, while also attempting to rescue Princess Leia from the mysterious Darth Vader.", 168 | Genre = new List { "Action", "Adventure", "Fantasy" }, 169 | CoverImage = "" 170 | }, 171 | new Movie 172 | { 173 | Id = 21, 174 | Title = "Star Wars: Episode V - The Empire Strikes Back", 175 | Description = "After the Rebels are overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jeditraining with Yoda, while his friends are pursued by Darth Vader and a bounty hunter named Boba Fett all over the galaxy.", 176 | Genre = new List { "Action", "Adventure", "Fantasy" }, 177 | CoverImage = "" 178 | }, 179 | new Movie 180 | { 181 | Id = 22, 182 | Title = "Star Wars: Episode VI - Return of the Jedi", 183 | Description = "After a daring mission to rescue Han Solo from Jabba the Hutt, the Rebels dispatch to Endor to destroy the second Death Star. Meanwhile, Luke struggles to help Darth Vader back from the dark side without falling into the Emperor's trap.", 184 | Genre = new List { "Action", "Adventure", "Fantasy" }, 185 | CoverImage = "" 186 | } 187 | ); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /MovieAPI/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official .NET 8.0 SDK image to build the app 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env 3 | WORKDIR /app 4 | 5 | # Copy everything and restore as distinct layers 6 | COPY *.csproj ./ 7 | RUN dotnet restore 8 | 9 | # Copy the rest of the project files and build the app 10 | COPY . ./ 11 | RUN dotnet publish -c Release -o out 12 | 13 | # Build runtime image 14 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 15 | WORKDIR /app 16 | COPY --from=build-env /app/out . 17 | 18 | # Expose the port on which the API will run 19 | EXPOSE 80 20 | 21 | # Set the entry point for the container 22 | ENTRYPOINT ["dotnet", "MovieAPI.dll"] 23 | -------------------------------------------------------------------------------- /MovieAPI/Extensions/SwaggerServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.OpenApi.Models; 3 | using System; 4 | using System.IO; 5 | using System.Reflection; 6 | 7 | namespace MovieAPI.Extensions 8 | { 9 | public static class SwaggerServiceExtensions 10 | { 11 | public static IServiceCollection AddCustomSwagger(this IServiceCollection services) 12 | { 13 | services.AddSwaggerGen(options => 14 | { 15 | options.SwaggerDoc("v1", new OpenApiInfo 16 | { 17 | Version = "v1", 18 | Title = "Movie API", 19 | Description = "An API to manage movies and their details", 20 | }); 21 | 22 | // Set the comments path for the Swagger JSON and UI. 23 | var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 24 | var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); 25 | options.IncludeXmlComments(xmlPath); 26 | 27 | // Enable annotations for Swagger documentation 28 | options.EnableAnnotations(); 29 | }); 30 | 31 | return services; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240815043414_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using MovieAPI.Data; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | #nullable disable 10 | 11 | namespace MovieAPI.Migrations 12 | { 13 | [DbContext(typeof(MovieContext))] 14 | [Migration("20240815043414_InitialCreate")] 15 | partial class InitialCreate 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "8.0.8") 23 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 24 | 25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 26 | 27 | modelBuilder.Entity("MovieAPI.Models.Movie", b => 28 | { 29 | b.Property("Id") 30 | .ValueGeneratedOnAdd() 31 | .HasColumnType("integer"); 32 | 33 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 34 | 35 | b.Property("CoverImage") 36 | .IsRequired() 37 | .HasColumnType("text"); 38 | 39 | b.Property("Description") 40 | .IsRequired() 41 | .HasColumnType("text"); 42 | 43 | b.Property("Genre") 44 | .IsRequired() 45 | .HasColumnType("text"); 46 | 47 | b.Property("Title") 48 | .IsRequired() 49 | .HasColumnType("text"); 50 | 51 | b.HasKey("Id"); 52 | 53 | b.ToTable("Movies"); 54 | }); 55 | #pragma warning restore 612, 618 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240815043414_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 3 | 4 | #nullable disable 5 | 6 | namespace MovieAPI.Migrations 7 | { 8 | /// 9 | public partial class InitialCreate : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Movies", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "integer", nullable: false) 19 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 20 | Title = table.Column(type: "text", nullable: false), 21 | Description = table.Column(type: "text", nullable: false), 22 | Genre = table.Column(type: "text", nullable: false), 23 | CoverImage = table.Column(type: "text", nullable: false) 24 | }, 25 | constraints: table => 26 | { 27 | table.PrimaryKey("PK_Movies", x => x.Id); 28 | }); 29 | } 30 | 31 | /// 32 | protected override void Down(MigrationBuilder migrationBuilder) 33 | { 34 | migrationBuilder.DropTable( 35 | name: "Movies"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240815061531_AddGenresToMovie.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using MovieAPI.Data; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | #nullable disable 10 | 11 | namespace MovieAPI.Migrations 12 | { 13 | [DbContext(typeof(MovieContext))] 14 | [Migration("20240815061531_AddGenresToMovie")] 15 | partial class AddGenresToMovie 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "8.0.8") 23 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 24 | 25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 26 | 27 | modelBuilder.Entity("MovieAPI.Models.Movie", b => 28 | { 29 | b.Property("Id") 30 | .ValueGeneratedOnAdd() 31 | .HasColumnType("integer"); 32 | 33 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 34 | 35 | b.Property("CoverImage") 36 | .HasColumnType("text"); 37 | 38 | b.Property("Description") 39 | .IsRequired() 40 | .HasColumnType("text"); 41 | 42 | b.Property("Genre") 43 | .IsRequired() 44 | .HasColumnType("text"); 45 | 46 | b.Property("Title") 47 | .IsRequired() 48 | .HasColumnType("text"); 49 | 50 | b.HasKey("Id"); 51 | 52 | b.ToTable("Movies"); 53 | }); 54 | #pragma warning restore 612, 618 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240815061531_AddGenresToMovie.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace MovieAPI.Migrations 6 | { 7 | /// 8 | public partial class AddGenresToMovie : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AlterColumn( 14 | name: "CoverImage", 15 | table: "Movies", 16 | type: "text", 17 | nullable: true, 18 | oldClrType: typeof(string), 19 | oldType: "text"); 20 | } 21 | 22 | /// 23 | protected override void Down(MigrationBuilder migrationBuilder) 24 | { 25 | migrationBuilder.AlterColumn( 26 | name: "CoverImage", 27 | table: "Movies", 28 | type: "text", 29 | nullable: false, 30 | defaultValue: "", 31 | oldClrType: typeof(string), 32 | oldType: "text", 33 | oldNullable: true); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240815101318_SeedInitialMovies.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using MovieAPI.Data; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | #nullable disable 10 | 11 | namespace MovieAPI.Migrations 12 | { 13 | [DbContext(typeof(MovieContext))] 14 | [Migration("20240815101318_SeedInitialMovies")] 15 | partial class SeedInitialMovies 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "8.0.8") 23 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 24 | 25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 26 | 27 | modelBuilder.Entity("MovieAPI.Models.Movie", b => 28 | { 29 | b.Property("Id") 30 | .ValueGeneratedOnAdd() 31 | .HasColumnType("integer"); 32 | 33 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 34 | 35 | b.Property("CoverImage") 36 | .HasColumnType("text"); 37 | 38 | b.Property("Description") 39 | .IsRequired() 40 | .HasColumnType("text"); 41 | 42 | b.Property("Genre") 43 | .IsRequired() 44 | .HasColumnType("text"); 45 | 46 | b.Property("Title") 47 | .IsRequired() 48 | .HasColumnType("text"); 49 | 50 | b.HasKey("Id"); 51 | 52 | b.ToTable("Movies"); 53 | 54 | b.HasData( 55 | new 56 | { 57 | Id = 1, 58 | CoverImage = "thedarkknight.jpg", 59 | Description = "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.", 60 | Genre = "Action,Drama,Thriller", 61 | Title = "The Dark Knight" 62 | }, 63 | new 64 | { 65 | Id = 2, 66 | CoverImage = "inception.jpg", 67 | Description = "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.", 68 | Genre = "Sci-Fi,Action,Thriller", 69 | Title = "Inception" 70 | }, 71 | new 72 | { 73 | Id = 3, 74 | CoverImage = "forrestgump.jpg", 75 | Description = "The presidencies of Kennedy and Johnson, the Vietnam War, the Watergate scandal and other historical events unfold from the perspective of an Alabama man with an IQ of 75, whose only desire is to be reunited with his childhood sweetheart.", 76 | Genre = "Drama,Romance", 77 | Title = "Forrest Gump" 78 | }, 79 | new 80 | { 81 | Id = 4, 82 | CoverImage = "up.jpg", 83 | Description = "Seventy-eight year old Carl Fredricksen travels to Paradise Falls in his house equipped with balloons, inadvertently taking a young stowaway.", 84 | Genre = "Animation,Adventure,Family", 85 | Title = "Up" 86 | }, 87 | new 88 | { 89 | Id = 5, 90 | CoverImage = "shawshankredemption.jpg", 91 | Description = "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", 92 | Genre = "Drama", 93 | Title = "The Shawshank Redemption" 94 | }, 95 | new 96 | { 97 | Id = 6, 98 | CoverImage = "godfather.jpg", 99 | Description = "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", 100 | Genre = "Crime,Drama", 101 | Title = "The Godfather" 102 | }, 103 | new 104 | { 105 | Id = 7, 106 | CoverImage = "godfather2.jpg", 107 | Description = "The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.", 108 | Genre = "Crime,Drama", 109 | Title = "The Godfather: Part II" 110 | }, 111 | new 112 | { 113 | Id = 8, 114 | CoverImage = "thedarkknightrises.jpg", 115 | Description = "Eight years after the Joker's reign of anarchy, Batman is forced from his exile with the help of the enigmatic Selina Kyle to save Gotham City from the brutal guerrilla terrorist Bane.", 116 | Genre = "Action,Drama,Thriller", 117 | Title = "The Dark Knight Rises" 118 | }, 119 | new 120 | { 121 | Id = 9, 122 | CoverImage = "interstellar.jpg", 123 | Description = "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", 124 | Genre = "Adventure,Drama,Sci-Fi", 125 | Title = "Interstellar" 126 | }, 127 | new 128 | { 129 | Id = 10, 130 | CoverImage = "gladiator.jpg", 131 | Description = "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family and sent him into slavery.", 132 | Genre = "Action,Adventure,Drama", 133 | Title = "Gladiator" 134 | }, 135 | new 136 | { 137 | Id = 11, 138 | CoverImage = "braveheart.jpg", 139 | Description = "Scottish warrior William Wallace leads his countrymen in a rebellion to free his homeland from the tyranny of King Edward I of England.", 140 | Genre = "Biography,Drama,History", 141 | Title = "Braveheart" 142 | }, 143 | new 144 | { 145 | Id = 12, 146 | CoverImage = "savingprivateryan.jpg", 147 | Description = "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action.", 148 | Genre = "Drama,War", 149 | Title = "Saving Private Ryan" 150 | }, 151 | new 152 | { 153 | Id = 13, 154 | CoverImage = "schindlerslist.jpg", 155 | Description = "In German-occupied Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.", 156 | Genre = "Biography,Drama,History", 157 | Title = "Schindler's List" 158 | }, 159 | new 160 | { 161 | Id = 14, 162 | CoverImage = "silenceofthelambs.jpg", 163 | Description = "A young FBI cadet must receive the help of an incarcerated and manipulative cannibal killer to help catch another serial killer, a madman who skins his victims.", 164 | Genre = "Crime,Drama,Thriller", 165 | Title = "The Silence of the Lambs" 166 | }, 167 | new 168 | { 169 | Id = 15, 170 | CoverImage = "fightclub.jpg", 171 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 172 | Genre = "Drama", 173 | Title = "Fight Club" 174 | }, 175 | new 176 | { 177 | Id = 16, 178 | CoverImage = "matrix.jpg", 179 | Description = "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", 180 | Genre = "Action,Sci-Fi", 181 | Title = "The Matrix" 182 | }, 183 | new 184 | { 185 | Id = 17, 186 | CoverImage = "lordoftherings3.jpg", 187 | Description = "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", 188 | Genre = "Adventure,Drama,Fantasy", 189 | Title = "The Lord of the Rings: The Return of the King" 190 | }, 191 | new 192 | { 193 | Id = 18, 194 | CoverImage = "lordoftherings2.jpg", 195 | Description = "While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard.", 196 | Genre = "Adventure,Drama,Fantasy", 197 | Title = "The Lord of the Rings: The Two Towers" 198 | }, 199 | new 200 | { 201 | Id = 19, 202 | CoverImage = "lordoftherings1.jpg", 203 | Description = "A meek Hobbit from the Shire and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron.", 204 | Genre = "Adventure,Drama,Fantasy", 205 | Title = "The Lord of the Rings: The Fellowship of the Ring" 206 | }, 207 | new 208 | { 209 | Id = 20, 210 | CoverImage = "starwars4.jpg", 211 | Description = "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee, and two droids to save the galaxy from the Empire's world-destroying battle station, while also attempting to rescue Princess Leia from the mysterious Darth Vader.", 212 | Genre = "Action,Adventure,Fantasy", 213 | Title = "Star Wars: Episode IV - A New Hope" 214 | }, 215 | new 216 | { 217 | Id = 21, 218 | CoverImage = "starwars5.jpg", 219 | Description = "After the Rebels are overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jeditraining with Yoda, while his friends are pursued by Darth Vader and a bounty hunter named Boba Fett all over the galaxy.", 220 | Genre = "Action,Adventure,Fantasy", 221 | Title = "Star Wars: Episode V - The Empire Strikes Back" 222 | }, 223 | new 224 | { 225 | Id = 22, 226 | CoverImage = "starwars6.jpg", 227 | Description = "After a daring mission to rescue Han Solo from Jabba the Hutt, the Rebels dispatch to Endor to destroy the second Death Star. Meanwhile, Luke struggles to help Darth Vader back from the dark side without falling into the Emperor's trap.", 228 | Genre = "Action,Adventure,Fantasy", 229 | Title = "Star Wars: Episode VI - Return of the Jedi" 230 | }); 231 | }); 232 | #pragma warning restore 612, 618 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240815101318_SeedInitialMovies.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | #pragma warning disable CA1814 // Prefer jagged arrays over multidimensional 6 | 7 | namespace MovieAPI.Migrations 8 | { 9 | /// 10 | public partial class SeedInitialMovies : Migration 11 | { 12 | /// 13 | protected override void Up(MigrationBuilder migrationBuilder) 14 | { 15 | migrationBuilder.InsertData( 16 | table: "Movies", 17 | columns: new[] { "Id", "CoverImage", "Description", "Genre", "Title" }, 18 | values: new object[,] 19 | { 20 | { 1, "thedarkknight.jpg", "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.", "Action,Drama,Thriller", "The Dark Knight" }, 21 | { 2, "inception.jpg", "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.", "Sci-Fi,Action,Thriller", "Inception" }, 22 | { 3, "forrestgump.jpg", "The presidencies of Kennedy and Johnson, the Vietnam War, the Watergate scandal and other historical events unfold from the perspective of an Alabama man with an IQ of 75, whose only desire is to be reunited with his childhood sweetheart.", "Drama,Romance", "Forrest Gump" }, 23 | { 4, "up.jpg", "Seventy-eight year old Carl Fredricksen travels to Paradise Falls in his house equipped with balloons, inadvertently taking a young stowaway.", "Animation,Adventure,Family", "Up" }, 24 | { 5, "shawshankredemption.jpg", "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", "Drama", "The Shawshank Redemption" }, 25 | { 6, "godfather.jpg", "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", "Crime,Drama", "The Godfather" }, 26 | { 7, "godfather2.jpg", "The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.", "Crime,Drama", "The Godfather: Part II" }, 27 | { 8, "thedarkknightrises.jpg", "Eight years after the Joker's reign of anarchy, Batman is forced from his exile with the help of the enigmatic Selina Kyle to save Gotham City from the brutal guerrilla terrorist Bane.", "Action,Drama,Thriller", "The Dark Knight Rises" }, 28 | { 9, "interstellar.jpg", "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", "Adventure,Drama,Sci-Fi", "Interstellar" }, 29 | { 10, "gladiator.jpg", "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family and sent him into slavery.", "Action,Adventure,Drama", "Gladiator" }, 30 | { 11, "braveheart.jpg", "Scottish warrior William Wallace leads his countrymen in a rebellion to free his homeland from the tyranny of King Edward I of England.", "Biography,Drama,History", "Braveheart" }, 31 | { 12, "savingprivateryan.jpg", "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action.", "Drama,War", "Saving Private Ryan" }, 32 | { 13, "schindlerslist.jpg", "In German-occupied Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.", "Biography,Drama,History", "Schindler's List" }, 33 | { 14, "silenceofthelambs.jpg", "A young FBI cadet must receive the help of an incarcerated and manipulative cannibal killer to help catch another serial killer, a madman who skins his victims.", "Crime,Drama,Thriller", "The Silence of the Lambs" }, 34 | { 15, "fightclub.jpg", "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", "Drama", "Fight Club" }, 35 | { 16, "matrix.jpg", "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", "Action,Sci-Fi", "The Matrix" }, 36 | { 17, "lordoftherings3.jpg", "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", "Adventure,Drama,Fantasy", "The Lord of the Rings: The Return of the King" }, 37 | { 18, "lordoftherings2.jpg", "While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard.", "Adventure,Drama,Fantasy", "The Lord of the Rings: The Two Towers" }, 38 | { 19, "lordoftherings1.jpg", "A meek Hobbit from the Shire and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron.", "Adventure,Drama,Fantasy", "The Lord of the Rings: The Fellowship of the Ring" }, 39 | { 20, "starwars4.jpg", "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee, and two droids to save the galaxy from the Empire's world-destroying battle station, while also attempting to rescue Princess Leia from the mysterious Darth Vader.", "Action,Adventure,Fantasy", "Star Wars: Episode IV - A New Hope" }, 40 | { 21, "starwars5.jpg", "After the Rebels are overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jeditraining with Yoda, while his friends are pursued by Darth Vader and a bounty hunter named Boba Fett all over the galaxy.", "Action,Adventure,Fantasy", "Star Wars: Episode V - The Empire Strikes Back" }, 41 | { 22, "starwars6.jpg", "After a daring mission to rescue Han Solo from Jabba the Hutt, the Rebels dispatch to Endor to destroy the second Death Star. Meanwhile, Luke struggles to help Darth Vader back from the dark side without falling into the Emperor's trap.", "Action,Adventure,Fantasy", "Star Wars: Episode VI - Return of the Jedi" } 42 | }); 43 | } 44 | 45 | /// 46 | protected override void Down(MigrationBuilder migrationBuilder) 47 | { 48 | migrationBuilder.DeleteData( 49 | table: "Movies", 50 | keyColumn: "Id", 51 | keyValue: 1); 52 | 53 | migrationBuilder.DeleteData( 54 | table: "Movies", 55 | keyColumn: "Id", 56 | keyValue: 2); 57 | 58 | migrationBuilder.DeleteData( 59 | table: "Movies", 60 | keyColumn: "Id", 61 | keyValue: 3); 62 | 63 | migrationBuilder.DeleteData( 64 | table: "Movies", 65 | keyColumn: "Id", 66 | keyValue: 4); 67 | 68 | migrationBuilder.DeleteData( 69 | table: "Movies", 70 | keyColumn: "Id", 71 | keyValue: 5); 72 | 73 | migrationBuilder.DeleteData( 74 | table: "Movies", 75 | keyColumn: "Id", 76 | keyValue: 6); 77 | 78 | migrationBuilder.DeleteData( 79 | table: "Movies", 80 | keyColumn: "Id", 81 | keyValue: 7); 82 | 83 | migrationBuilder.DeleteData( 84 | table: "Movies", 85 | keyColumn: "Id", 86 | keyValue: 8); 87 | 88 | migrationBuilder.DeleteData( 89 | table: "Movies", 90 | keyColumn: "Id", 91 | keyValue: 9); 92 | 93 | migrationBuilder.DeleteData( 94 | table: "Movies", 95 | keyColumn: "Id", 96 | keyValue: 10); 97 | 98 | migrationBuilder.DeleteData( 99 | table: "Movies", 100 | keyColumn: "Id", 101 | keyValue: 11); 102 | 103 | migrationBuilder.DeleteData( 104 | table: "Movies", 105 | keyColumn: "Id", 106 | keyValue: 12); 107 | 108 | migrationBuilder.DeleteData( 109 | table: "Movies", 110 | keyColumn: "Id", 111 | keyValue: 13); 112 | 113 | migrationBuilder.DeleteData( 114 | table: "Movies", 115 | keyColumn: "Id", 116 | keyValue: 14); 117 | 118 | migrationBuilder.DeleteData( 119 | table: "Movies", 120 | keyColumn: "Id", 121 | keyValue: 15); 122 | 123 | migrationBuilder.DeleteData( 124 | table: "Movies", 125 | keyColumn: "Id", 126 | keyValue: 16); 127 | 128 | migrationBuilder.DeleteData( 129 | table: "Movies", 130 | keyColumn: "Id", 131 | keyValue: 17); 132 | 133 | migrationBuilder.DeleteData( 134 | table: "Movies", 135 | keyColumn: "Id", 136 | keyValue: 18); 137 | 138 | migrationBuilder.DeleteData( 139 | table: "Movies", 140 | keyColumn: "Id", 141 | keyValue: 19); 142 | 143 | migrationBuilder.DeleteData( 144 | table: "Movies", 145 | keyColumn: "Id", 146 | keyValue: 20); 147 | 148 | migrationBuilder.DeleteData( 149 | table: "Movies", 150 | keyColumn: "Id", 151 | keyValue: 21); 152 | 153 | migrationBuilder.DeleteData( 154 | table: "Movies", 155 | keyColumn: "Id", 156 | keyValue: 22); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240816111759_RemoveImageUrl.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using MovieAPI.Data; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | #nullable disable 10 | 11 | namespace MovieAPI.Migrations 12 | { 13 | [DbContext(typeof(MovieContext))] 14 | [Migration("20240816111759_RemoveImageUrl")] 15 | partial class RemoveImageUrl 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "8.0.8") 23 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 24 | 25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 26 | 27 | modelBuilder.Entity("MovieAPI.Models.Movie", b => 28 | { 29 | b.Property("Id") 30 | .ValueGeneratedOnAdd() 31 | .HasColumnType("integer"); 32 | 33 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 34 | 35 | b.Property("CoverImage") 36 | .HasColumnType("text"); 37 | 38 | b.Property("Description") 39 | .IsRequired() 40 | .HasColumnType("text"); 41 | 42 | b.Property("Genre") 43 | .IsRequired() 44 | .HasColumnType("text"); 45 | 46 | b.Property("Title") 47 | .IsRequired() 48 | .HasColumnType("text"); 49 | 50 | b.HasKey("Id"); 51 | 52 | b.ToTable("Movies"); 53 | 54 | b.HasData( 55 | new 56 | { 57 | Id = 1, 58 | CoverImage = "", 59 | Description = "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.", 60 | Genre = "Action,Drama,Thriller", 61 | Title = "The Dark Knight" 62 | }, 63 | new 64 | { 65 | Id = 2, 66 | CoverImage = "", 67 | Description = "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.", 68 | Genre = "Sci-Fi,Action,Thriller", 69 | Title = "Inception" 70 | }, 71 | new 72 | { 73 | Id = 3, 74 | CoverImage = "", 75 | Description = "The presidencies of Kennedy and Johnson, the Vietnam War, the Watergate scandal and other historical events unfold from the perspective of an Alabama man with an IQ of 75, whose only desire is to be reunited with his childhood sweetheart.", 76 | Genre = "Drama,Romance", 77 | Title = "Forrest Gump" 78 | }, 79 | new 80 | { 81 | Id = 4, 82 | CoverImage = "", 83 | Description = "Seventy-eight year old Carl Fredricksen travels to Paradise Falls in his house equipped with balloons, inadvertently taking a young stowaway.", 84 | Genre = "Animation,Adventure,Family", 85 | Title = "Up" 86 | }, 87 | new 88 | { 89 | Id = 5, 90 | CoverImage = "", 91 | Description = "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", 92 | Genre = "Drama", 93 | Title = "The Shawshank Redemption" 94 | }, 95 | new 96 | { 97 | Id = 6, 98 | CoverImage = "", 99 | Description = "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", 100 | Genre = "Crime,Drama", 101 | Title = "The Godfather" 102 | }, 103 | new 104 | { 105 | Id = 7, 106 | CoverImage = "", 107 | Description = "The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.", 108 | Genre = "Crime,Drama", 109 | Title = "The Godfather: Part II" 110 | }, 111 | new 112 | { 113 | Id = 8, 114 | CoverImage = "", 115 | Description = "Eight years after the Joker's reign of anarchy, Batman is forced from his exile with the help of the enigmatic Selina Kyle to save Gotham City from the brutal guerrilla terrorist Bane.", 116 | Genre = "Action,Drama,Thriller", 117 | Title = "The Dark Knight Rises" 118 | }, 119 | new 120 | { 121 | Id = 9, 122 | CoverImage = "", 123 | Description = "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", 124 | Genre = "Adventure,Drama,Sci-Fi", 125 | Title = "Interstellar" 126 | }, 127 | new 128 | { 129 | Id = 10, 130 | CoverImage = "", 131 | Description = "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family and sent him into slavery.", 132 | Genre = "Action,Adventure,Drama", 133 | Title = "Gladiator" 134 | }, 135 | new 136 | { 137 | Id = 11, 138 | CoverImage = "", 139 | Description = "Scottish warrior William Wallace leads his countrymen in a rebellion to free his homeland from the tyranny of King Edward I of England.", 140 | Genre = "Biography,Drama,History", 141 | Title = "Braveheart" 142 | }, 143 | new 144 | { 145 | Id = 12, 146 | CoverImage = "", 147 | Description = "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action.", 148 | Genre = "Drama,War", 149 | Title = "Saving Private Ryan" 150 | }, 151 | new 152 | { 153 | Id = 13, 154 | CoverImage = "", 155 | Description = "In German-occupied Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.", 156 | Genre = "Biography,Drama,History", 157 | Title = "Schindler's List" 158 | }, 159 | new 160 | { 161 | Id = 14, 162 | CoverImage = "", 163 | Description = "A young FBI cadet must receive the help of an incarcerated and manipulative cannibal killer to help catch another serial killer, a madman who skins his victims.", 164 | Genre = "Crime,Drama,Thriller", 165 | Title = "The Silence of the Lambs" 166 | }, 167 | new 168 | { 169 | Id = 15, 170 | CoverImage = "", 171 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 172 | Genre = "Drama", 173 | Title = "Fight Club" 174 | }, 175 | new 176 | { 177 | Id = 16, 178 | CoverImage = "", 179 | Description = "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", 180 | Genre = "Action,Sci-Fi", 181 | Title = "The Matrix" 182 | }, 183 | new 184 | { 185 | Id = 17, 186 | CoverImage = "", 187 | Description = "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", 188 | Genre = "Adventure,Drama,Fantasy", 189 | Title = "The Lord of the Rings: The Return of the King" 190 | }, 191 | new 192 | { 193 | Id = 18, 194 | CoverImage = "", 195 | Description = "While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard.", 196 | Genre = "Adventure,Drama,Fantasy", 197 | Title = "The Lord of the Rings: The Two Towers" 198 | }, 199 | new 200 | { 201 | Id = 19, 202 | CoverImage = "", 203 | Description = "A meek Hobbit from the Shire and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron.", 204 | Genre = "Adventure,Drama,Fantasy", 205 | Title = "The Lord of the Rings: The Fellowship of the Ring" 206 | }, 207 | new 208 | { 209 | Id = 20, 210 | CoverImage = "", 211 | Description = "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee, and two droids to save the galaxy from the Empire's world-destroying battle station, while also attempting to rescue Princess Leia from the mysterious Darth Vader.", 212 | Genre = "Action,Adventure,Fantasy", 213 | Title = "Star Wars: Episode IV - A New Hope" 214 | }, 215 | new 216 | { 217 | Id = 21, 218 | CoverImage = "", 219 | Description = "After the Rebels are overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jeditraining with Yoda, while his friends are pursued by Darth Vader and a bounty hunter named Boba Fett all over the galaxy.", 220 | Genre = "Action,Adventure,Fantasy", 221 | Title = "Star Wars: Episode V - The Empire Strikes Back" 222 | }, 223 | new 224 | { 225 | Id = 22, 226 | CoverImage = "", 227 | Description = "After a daring mission to rescue Han Solo from Jabba the Hutt, the Rebels dispatch to Endor to destroy the second Death Star. Meanwhile, Luke struggles to help Darth Vader back from the dark side without falling into the Emperor's trap.", 228 | Genre = "Action,Adventure,Fantasy", 229 | Title = "Star Wars: Episode VI - Return of the Jedi" 230 | }); 231 | }); 232 | #pragma warning restore 612, 618 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/20240816111759_RemoveImageUrl.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace MovieAPI.Migrations 6 | { 7 | /// 8 | public partial class RemoveImageUrl : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.UpdateData( 14 | table: "Movies", 15 | keyColumn: "Id", 16 | keyValue: 1, 17 | column: "CoverImage", 18 | value: ""); 19 | 20 | migrationBuilder.UpdateData( 21 | table: "Movies", 22 | keyColumn: "Id", 23 | keyValue: 2, 24 | column: "CoverImage", 25 | value: ""); 26 | 27 | migrationBuilder.UpdateData( 28 | table: "Movies", 29 | keyColumn: "Id", 30 | keyValue: 3, 31 | column: "CoverImage", 32 | value: ""); 33 | 34 | migrationBuilder.UpdateData( 35 | table: "Movies", 36 | keyColumn: "Id", 37 | keyValue: 4, 38 | column: "CoverImage", 39 | value: ""); 40 | 41 | migrationBuilder.UpdateData( 42 | table: "Movies", 43 | keyColumn: "Id", 44 | keyValue: 5, 45 | column: "CoverImage", 46 | value: ""); 47 | 48 | migrationBuilder.UpdateData( 49 | table: "Movies", 50 | keyColumn: "Id", 51 | keyValue: 6, 52 | column: "CoverImage", 53 | value: ""); 54 | 55 | migrationBuilder.UpdateData( 56 | table: "Movies", 57 | keyColumn: "Id", 58 | keyValue: 7, 59 | column: "CoverImage", 60 | value: ""); 61 | 62 | migrationBuilder.UpdateData( 63 | table: "Movies", 64 | keyColumn: "Id", 65 | keyValue: 8, 66 | column: "CoverImage", 67 | value: ""); 68 | 69 | migrationBuilder.UpdateData( 70 | table: "Movies", 71 | keyColumn: "Id", 72 | keyValue: 9, 73 | column: "CoverImage", 74 | value: ""); 75 | 76 | migrationBuilder.UpdateData( 77 | table: "Movies", 78 | keyColumn: "Id", 79 | keyValue: 10, 80 | column: "CoverImage", 81 | value: ""); 82 | 83 | migrationBuilder.UpdateData( 84 | table: "Movies", 85 | keyColumn: "Id", 86 | keyValue: 11, 87 | column: "CoverImage", 88 | value: ""); 89 | 90 | migrationBuilder.UpdateData( 91 | table: "Movies", 92 | keyColumn: "Id", 93 | keyValue: 12, 94 | column: "CoverImage", 95 | value: ""); 96 | 97 | migrationBuilder.UpdateData( 98 | table: "Movies", 99 | keyColumn: "Id", 100 | keyValue: 13, 101 | column: "CoverImage", 102 | value: ""); 103 | 104 | migrationBuilder.UpdateData( 105 | table: "Movies", 106 | keyColumn: "Id", 107 | keyValue: 14, 108 | column: "CoverImage", 109 | value: ""); 110 | 111 | migrationBuilder.UpdateData( 112 | table: "Movies", 113 | keyColumn: "Id", 114 | keyValue: 15, 115 | column: "CoverImage", 116 | value: ""); 117 | 118 | migrationBuilder.UpdateData( 119 | table: "Movies", 120 | keyColumn: "Id", 121 | keyValue: 16, 122 | column: "CoverImage", 123 | value: ""); 124 | 125 | migrationBuilder.UpdateData( 126 | table: "Movies", 127 | keyColumn: "Id", 128 | keyValue: 17, 129 | column: "CoverImage", 130 | value: ""); 131 | 132 | migrationBuilder.UpdateData( 133 | table: "Movies", 134 | keyColumn: "Id", 135 | keyValue: 18, 136 | column: "CoverImage", 137 | value: ""); 138 | 139 | migrationBuilder.UpdateData( 140 | table: "Movies", 141 | keyColumn: "Id", 142 | keyValue: 19, 143 | column: "CoverImage", 144 | value: ""); 145 | 146 | migrationBuilder.UpdateData( 147 | table: "Movies", 148 | keyColumn: "Id", 149 | keyValue: 20, 150 | column: "CoverImage", 151 | value: ""); 152 | 153 | migrationBuilder.UpdateData( 154 | table: "Movies", 155 | keyColumn: "Id", 156 | keyValue: 21, 157 | column: "CoverImage", 158 | value: ""); 159 | 160 | migrationBuilder.UpdateData( 161 | table: "Movies", 162 | keyColumn: "Id", 163 | keyValue: 22, 164 | column: "CoverImage", 165 | value: ""); 166 | } 167 | 168 | /// 169 | protected override void Down(MigrationBuilder migrationBuilder) 170 | { 171 | migrationBuilder.UpdateData( 172 | table: "Movies", 173 | keyColumn: "Id", 174 | keyValue: 1, 175 | column: "CoverImage", 176 | value: "thedarkknight.jpg"); 177 | 178 | migrationBuilder.UpdateData( 179 | table: "Movies", 180 | keyColumn: "Id", 181 | keyValue: 2, 182 | column: "CoverImage", 183 | value: "inception.jpg"); 184 | 185 | migrationBuilder.UpdateData( 186 | table: "Movies", 187 | keyColumn: "Id", 188 | keyValue: 3, 189 | column: "CoverImage", 190 | value: "forrestgump.jpg"); 191 | 192 | migrationBuilder.UpdateData( 193 | table: "Movies", 194 | keyColumn: "Id", 195 | keyValue: 4, 196 | column: "CoverImage", 197 | value: "up.jpg"); 198 | 199 | migrationBuilder.UpdateData( 200 | table: "Movies", 201 | keyColumn: "Id", 202 | keyValue: 5, 203 | column: "CoverImage", 204 | value: "shawshankredemption.jpg"); 205 | 206 | migrationBuilder.UpdateData( 207 | table: "Movies", 208 | keyColumn: "Id", 209 | keyValue: 6, 210 | column: "CoverImage", 211 | value: "godfather.jpg"); 212 | 213 | migrationBuilder.UpdateData( 214 | table: "Movies", 215 | keyColumn: "Id", 216 | keyValue: 7, 217 | column: "CoverImage", 218 | value: "godfather2.jpg"); 219 | 220 | migrationBuilder.UpdateData( 221 | table: "Movies", 222 | keyColumn: "Id", 223 | keyValue: 8, 224 | column: "CoverImage", 225 | value: "thedarkknightrises.jpg"); 226 | 227 | migrationBuilder.UpdateData( 228 | table: "Movies", 229 | keyColumn: "Id", 230 | keyValue: 9, 231 | column: "CoverImage", 232 | value: "interstellar.jpg"); 233 | 234 | migrationBuilder.UpdateData( 235 | table: "Movies", 236 | keyColumn: "Id", 237 | keyValue: 10, 238 | column: "CoverImage", 239 | value: "gladiator.jpg"); 240 | 241 | migrationBuilder.UpdateData( 242 | table: "Movies", 243 | keyColumn: "Id", 244 | keyValue: 11, 245 | column: "CoverImage", 246 | value: "braveheart.jpg"); 247 | 248 | migrationBuilder.UpdateData( 249 | table: "Movies", 250 | keyColumn: "Id", 251 | keyValue: 12, 252 | column: "CoverImage", 253 | value: "savingprivateryan.jpg"); 254 | 255 | migrationBuilder.UpdateData( 256 | table: "Movies", 257 | keyColumn: "Id", 258 | keyValue: 13, 259 | column: "CoverImage", 260 | value: "schindlerslist.jpg"); 261 | 262 | migrationBuilder.UpdateData( 263 | table: "Movies", 264 | keyColumn: "Id", 265 | keyValue: 14, 266 | column: "CoverImage", 267 | value: "silenceofthelambs.jpg"); 268 | 269 | migrationBuilder.UpdateData( 270 | table: "Movies", 271 | keyColumn: "Id", 272 | keyValue: 15, 273 | column: "CoverImage", 274 | value: "fightclub.jpg"); 275 | 276 | migrationBuilder.UpdateData( 277 | table: "Movies", 278 | keyColumn: "Id", 279 | keyValue: 16, 280 | column: "CoverImage", 281 | value: "matrix.jpg"); 282 | 283 | migrationBuilder.UpdateData( 284 | table: "Movies", 285 | keyColumn: "Id", 286 | keyValue: 17, 287 | column: "CoverImage", 288 | value: "lordoftherings3.jpg"); 289 | 290 | migrationBuilder.UpdateData( 291 | table: "Movies", 292 | keyColumn: "Id", 293 | keyValue: 18, 294 | column: "CoverImage", 295 | value: "lordoftherings2.jpg"); 296 | 297 | migrationBuilder.UpdateData( 298 | table: "Movies", 299 | keyColumn: "Id", 300 | keyValue: 19, 301 | column: "CoverImage", 302 | value: "lordoftherings1.jpg"); 303 | 304 | migrationBuilder.UpdateData( 305 | table: "Movies", 306 | keyColumn: "Id", 307 | keyValue: 20, 308 | column: "CoverImage", 309 | value: "starwars4.jpg"); 310 | 311 | migrationBuilder.UpdateData( 312 | table: "Movies", 313 | keyColumn: "Id", 314 | keyValue: 21, 315 | column: "CoverImage", 316 | value: "starwars5.jpg"); 317 | 318 | migrationBuilder.UpdateData( 319 | table: "Movies", 320 | keyColumn: "Id", 321 | keyValue: 22, 322 | column: "CoverImage", 323 | value: "starwars6.jpg"); 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /MovieAPI/Migrations/MovieContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | using MovieAPI.Data; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 7 | 8 | #nullable disable 9 | 10 | namespace MovieAPI.Migrations 11 | { 12 | [DbContext(typeof(MovieContext))] 13 | partial class MovieContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "8.0.8") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 21 | 22 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 23 | 24 | modelBuilder.Entity("MovieAPI.Models.Movie", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasColumnType("integer"); 29 | 30 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 31 | 32 | b.Property("CoverImage") 33 | .HasColumnType("text"); 34 | 35 | b.Property("Description") 36 | .IsRequired() 37 | .HasColumnType("text"); 38 | 39 | b.Property("Genre") 40 | .IsRequired() 41 | .HasColumnType("text"); 42 | 43 | b.Property("Title") 44 | .IsRequired() 45 | .HasColumnType("text"); 46 | 47 | b.HasKey("Id"); 48 | 49 | b.ToTable("Movies"); 50 | 51 | b.HasData( 52 | new 53 | { 54 | Id = 1, 55 | CoverImage = "", 56 | Description = "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.", 57 | Genre = "Action,Drama,Thriller", 58 | Title = "The Dark Knight" 59 | }, 60 | new 61 | { 62 | Id = 2, 63 | CoverImage = "", 64 | Description = "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.", 65 | Genre = "Sci-Fi,Action,Thriller", 66 | Title = "Inception" 67 | }, 68 | new 69 | { 70 | Id = 3, 71 | CoverImage = "", 72 | Description = "The presidencies of Kennedy and Johnson, the Vietnam War, the Watergate scandal and other historical events unfold from the perspective of an Alabama man with an IQ of 75, whose only desire is to be reunited with his childhood sweetheart.", 73 | Genre = "Drama,Romance", 74 | Title = "Forrest Gump" 75 | }, 76 | new 77 | { 78 | Id = 4, 79 | CoverImage = "", 80 | Description = "Seventy-eight year old Carl Fredricksen travels to Paradise Falls in his house equipped with balloons, inadvertently taking a young stowaway.", 81 | Genre = "Animation,Adventure,Family", 82 | Title = "Up" 83 | }, 84 | new 85 | { 86 | Id = 5, 87 | CoverImage = "", 88 | Description = "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.", 89 | Genre = "Drama", 90 | Title = "The Shawshank Redemption" 91 | }, 92 | new 93 | { 94 | Id = 6, 95 | CoverImage = "", 96 | Description = "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.", 97 | Genre = "Crime,Drama", 98 | Title = "The Godfather" 99 | }, 100 | new 101 | { 102 | Id = 7, 103 | CoverImage = "", 104 | Description = "The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.", 105 | Genre = "Crime,Drama", 106 | Title = "The Godfather: Part II" 107 | }, 108 | new 109 | { 110 | Id = 8, 111 | CoverImage = "", 112 | Description = "Eight years after the Joker's reign of anarchy, Batman is forced from his exile with the help of the enigmatic Selina Kyle to save Gotham City from the brutal guerrilla terrorist Bane.", 113 | Genre = "Action,Drama,Thriller", 114 | Title = "The Dark Knight Rises" 115 | }, 116 | new 117 | { 118 | Id = 9, 119 | CoverImage = "", 120 | Description = "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.", 121 | Genre = "Adventure,Drama,Sci-Fi", 122 | Title = "Interstellar" 123 | }, 124 | new 125 | { 126 | Id = 10, 127 | CoverImage = "", 128 | Description = "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family and sent him into slavery.", 129 | Genre = "Action,Adventure,Drama", 130 | Title = "Gladiator" 131 | }, 132 | new 133 | { 134 | Id = 11, 135 | CoverImage = "", 136 | Description = "Scottish warrior William Wallace leads his countrymen in a rebellion to free his homeland from the tyranny of King Edward I of England.", 137 | Genre = "Biography,Drama,History", 138 | Title = "Braveheart" 139 | }, 140 | new 141 | { 142 | Id = 12, 143 | CoverImage = "", 144 | Description = "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action.", 145 | Genre = "Drama,War", 146 | Title = "Saving Private Ryan" 147 | }, 148 | new 149 | { 150 | Id = 13, 151 | CoverImage = "", 152 | Description = "In German-occupied Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.", 153 | Genre = "Biography,Drama,History", 154 | Title = "Schindler's List" 155 | }, 156 | new 157 | { 158 | Id = 14, 159 | CoverImage = "", 160 | Description = "A young FBI cadet must receive the help of an incarcerated and manipulative cannibal killer to help catch another serial killer, a madman who skins his victims.", 161 | Genre = "Crime,Drama,Thriller", 162 | Title = "The Silence of the Lambs" 163 | }, 164 | new 165 | { 166 | Id = 15, 167 | CoverImage = "", 168 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 169 | Genre = "Drama", 170 | Title = "Fight Club" 171 | }, 172 | new 173 | { 174 | Id = 16, 175 | CoverImage = "", 176 | Description = "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", 177 | Genre = "Action,Sci-Fi", 178 | Title = "The Matrix" 179 | }, 180 | new 181 | { 182 | Id = 17, 183 | CoverImage = "", 184 | Description = "Gandalf and Aragorn lead the World of Men against Sauron's army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.", 185 | Genre = "Adventure,Drama,Fantasy", 186 | Title = "The Lord of the Rings: The Return of the King" 187 | }, 188 | new 189 | { 190 | Id = 18, 191 | CoverImage = "", 192 | Description = "While Frodo and Sam edge closer to Mordor with the help of the shifty Gollum, the divided fellowship makes a stand against Sauron's new ally, Saruman, and his hordes of Isengard.", 193 | Genre = "Adventure,Drama,Fantasy", 194 | Title = "The Lord of the Rings: The Two Towers" 195 | }, 196 | new 197 | { 198 | Id = 19, 199 | CoverImage = "", 200 | Description = "A meek Hobbit from the Shire and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron.", 201 | Genre = "Adventure,Drama,Fantasy", 202 | Title = "The Lord of the Rings: The Fellowship of the Ring" 203 | }, 204 | new 205 | { 206 | Id = 20, 207 | CoverImage = "", 208 | Description = "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee, and two droids to save the galaxy from the Empire's world-destroying battle station, while also attempting to rescue Princess Leia from the mysterious Darth Vader.", 209 | Genre = "Action,Adventure,Fantasy", 210 | Title = "Star Wars: Episode IV - A New Hope" 211 | }, 212 | new 213 | { 214 | Id = 21, 215 | CoverImage = "", 216 | Description = "After the Rebels are overpowered by the Empire on the ice planet Hoth, Luke Skywalker begins Jeditraining with Yoda, while his friends are pursued by Darth Vader and a bounty hunter named Boba Fett all over the galaxy.", 217 | Genre = "Action,Adventure,Fantasy", 218 | Title = "Star Wars: Episode V - The Empire Strikes Back" 219 | }, 220 | new 221 | { 222 | Id = 22, 223 | CoverImage = "", 224 | Description = "After a daring mission to rescue Han Solo from Jabba the Hutt, the Rebels dispatch to Endor to destroy the second Death Star. Meanwhile, Luke struggles to help Darth Vader back from the dark side without falling into the Emperor's trap.", 225 | Genre = "Action,Adventure,Fantasy", 226 | Title = "Star Wars: Episode VI - Return of the Jedi" 227 | }); 228 | }); 229 | #pragma warning restore 612, 618 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /MovieAPI/Models/Movie.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace MovieAPI.Models 5 | { 6 | public class Movie 7 | { 8 | public int Id { get; set; } 9 | 10 | [Required] 11 | public string Title { get; set; } = string.Empty; 12 | 13 | [Required] 14 | public string Description { get; set; } = string.Empty; 15 | 16 | [Required] 17 | public List Genre { get; set; } = new List(); // Updated to List 18 | 19 | public string? CoverImage { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MovieAPI/Models/PaginatedResponse.cs: -------------------------------------------------------------------------------- 1 | namespace MovieAPI.Models 2 | { 3 | public class PaginatedResponse 4 | { 5 | public IEnumerable Items { get; set; } 6 | public int TotalCount { get; set; } 7 | 8 | public PaginatedResponse(IEnumerable items, int totalCount) 9 | { 10 | Items = items; 11 | TotalCount = totalCount; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MovieAPI/MovieAPI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | $(NoWarn);1591 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /MovieAPI/MovieAPI.generated.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MovieAPI", "MovieAPI\MovieAPI.csproj", "{5720DE74-F124-4787-9564-DFAB17C2C642}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {5720DE74-F124-4787-9564-DFAB17C2C642}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {5720DE74-F124-4787-9564-DFAB17C2C642}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {5720DE74-F124-4787-9564-DFAB17C2C642}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {5720DE74-F124-4787-9564-DFAB17C2C642}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {09CE92E4-0911-4AF2-BBB9-42D9468C7565} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /MovieAPI/MovieAPI.http: -------------------------------------------------------------------------------- 1 | @MovieAPI_HostAddress = http://localhost:5103 2 | 3 | GET {{MovieAPI_HostAddress}}/weatherforecast/ 4 | Accept: application/json 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /MovieAPI/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.EntityFrameworkCore; 3 | using MovieAPI.Data; 4 | using MovieAPI.Services; 5 | using MovieAPI.Repositories; 6 | using MovieAPI.Extensions; 7 | using Microsoft.Extensions.FileProviders; 8 | 9 | var builder = WebApplication.CreateBuilder(args); 10 | 11 | // Add services to the container. 12 | builder.Services.AddControllers(); 13 | builder.Services.AddEndpointsApiExplorer(); 14 | 15 | // Use custom Swagger configuration 16 | builder.Services.AddCustomSwagger(); 17 | 18 | // Configure DbContext 19 | builder.Services.AddDbContext(options => 20 | options.UseNpgsql(builder.Configuration.GetConnectionString("MovieDbConnection"))); 21 | 22 | // Register services and repositories 23 | builder.Services.AddScoped(); 24 | builder.Services.AddScoped(); 25 | 26 | // Enable CORS 27 | builder.Services.AddCors(options => 28 | { 29 | options.AddPolicy("AllowSpecificOrigin", 30 | builder => builder.WithOrigins("http://localhost:4200") // Replace with your front-end URL 31 | .AllowAnyHeader() 32 | .AllowAnyMethod()); 33 | }); 34 | 35 | var app = builder.Build(); 36 | 37 | // Configure the HTTP request pipeline. 38 | if (app.Environment.IsDevelopment()) 39 | { 40 | app.UseSwagger(); 41 | app.UseSwaggerUI(c => 42 | { 43 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "Movie API v1"); 44 | c.RoutePrefix = string.Empty; // Set Swagger UI at the root 45 | }); 46 | } 47 | 48 | // Serve static files from wwwroot directory 49 | app.UseStaticFiles(); 50 | 51 | // Serve static files from wwwroot/images directory with a specific path 52 | app.UseStaticFiles(new StaticFileOptions 53 | { 54 | FileProvider = new PhysicalFileProvider( 55 | Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images")), 56 | RequestPath = "/images" 57 | }); 58 | 59 | app.UseHttpsRedirection(); 60 | 61 | // Use the CORS policy 62 | app.UseCors("AllowSpecificOrigin"); 63 | 64 | app.UseAuthorization(); 65 | 66 | app.MapControllers(); 67 | 68 | app.Run(); 69 | -------------------------------------------------------------------------------- /MovieAPI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:59780", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5103", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MovieAPI/README.md: -------------------------------------------------------------------------------- 1 | # MovieAPI 2 | 3 | ## How to Run the Project 4 | 5 | ### Option 1: Running with Docker 6 | 7 | #### Build and Run the Containers: 8 | 9 | ```bash 10 | docker-compose up -d 11 | ``` 12 | 13 | #### Access the API: 14 | 15 | - API: [http://localhost:5103](http://localhost:5103) 16 | - Swagger UI: [http://localhost:5103/index.html](http://localhost:5103/index.html) 17 | 18 | ### Option 2: Running Without Docker 19 | 20 | #### Set Up PostgreSQL: 21 | 22 | - Ensure PostgreSQL is installed and running on your machine. 23 | - Create a database named `movie_db`. 24 | 25 | #### Update the Connection String: 26 | 27 | Modify the `ConnectionStrings.MovieDbConnection` in `appsettings.Development.json`: 28 | 29 | ```json 30 | "ConnectionStrings": { 31 | "MovieDbConnection": "Host=localhost;Database=movie_db;Username=[username];Password=[password]" 32 | } 33 | ``` 34 | 35 | #### Run the Migrations: 36 | 37 | ```bash 38 | dotnet ef database update 39 | ``` 40 | 41 | #### Run the Application: 42 | 43 | ```bash 44 | dotnet run 45 | ``` 46 | 47 | #### Access the API: 48 | 49 | - API: [http://localhost:5103](http://localhost:5103) 50 | - Swagger UI: [http://localhost:5103/index.html](http://localhost:5103/index.html) -------------------------------------------------------------------------------- /MovieAPI/Repositories/IMovieRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using MovieAPI.Models; // Add this line 4 | 5 | namespace MovieAPI.Repositories 6 | { 7 | public interface IMovieRepository 8 | { 9 | Task> GetAllMoviesAsync(int pageNumber, int pageSize); 10 | Task GetMovieByIdAsync(int id); 11 | Task AddMovieAsync(Movie movie); 12 | Task UpdateMovieAsync(Movie movie); 13 | Task DeleteMovieAsync(Movie movie); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MovieAPI/Repositories/MovieRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | using MovieAPI.Data; // Add this line 5 | using MovieAPI.Models; // Add this line 6 | 7 | namespace MovieAPI.Repositories 8 | { 9 | public class MovieRepository : IMovieRepository 10 | { 11 | private readonly MovieContext _context; 12 | 13 | public MovieRepository(MovieContext context) 14 | { 15 | _context = context; 16 | } 17 | 18 | public async Task> GetAllMoviesAsync(int pageNumber, int pageSize) 19 | { 20 | var totalCount = await _context.Movies.CountAsync(); 21 | var movies = await _context.Movies 22 | .OrderByDescending(m => m.Id) 23 | .Skip((pageNumber - 1) * pageSize) 24 | .Take(pageSize) 25 | .ToListAsync(); 26 | 27 | return new PaginatedResponse(movies, totalCount); 28 | } 29 | 30 | public async Task GetMovieByIdAsync(int id) 31 | { 32 | return await _context.Movies.FindAsync(id); 33 | } 34 | 35 | public async Task AddMovieAsync(Movie movie) 36 | { 37 | _context.Movies.Add(movie); 38 | await _context.SaveChangesAsync(); 39 | return movie; 40 | } 41 | 42 | public async Task UpdateMovieAsync(Movie movie) 43 | { 44 | _context.Entry(movie).State = EntityState.Modified; 45 | await _context.SaveChangesAsync(); 46 | } 47 | 48 | public async Task DeleteMovieAsync(Movie movie) 49 | { 50 | _context.Movies.Remove(movie); 51 | await _context.SaveChangesAsync(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MovieAPI/Services/IMovieService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using MovieAPI.Models; 3 | using System.Threading.Tasks; 4 | 5 | namespace MovieAPI.Services 6 | { 7 | public interface IMovieService 8 | { 9 | Task> GetAllMoviesAsync(int pageNumber, int pageSize); 10 | Task GetMovieByIdAsync(int id); 11 | Task CreateMovieAsync(Movie movie, IFormFile coverImage); 12 | Task UpdateMovieAsync(Movie movie, IFormFile? coverImage); 13 | Task DeleteMovieAsync(int id); 14 | Task UploadCoverImageAsync(IFormFile coverImage); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MovieAPI/Services/MovieService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using MovieAPI.Models; 3 | using MovieAPI.Repositories; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | 9 | namespace MovieAPI.Services 10 | { 11 | public class MovieService : IMovieService 12 | { 13 | private readonly IMovieRepository _movieRepository; 14 | private readonly string _imageDirectory = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images"); 15 | private readonly string _imagePathPrefix = "/images/"; 16 | 17 | public MovieService(IMovieRepository movieRepository) 18 | { 19 | _movieRepository = movieRepository; 20 | } 21 | 22 | public async Task> GetAllMoviesAsync(int pageNumber, int pageSize) 23 | { 24 | return await _movieRepository.GetAllMoviesAsync(pageNumber, pageSize); 25 | } 26 | 27 | public async Task GetMovieByIdAsync(int id) 28 | { 29 | return await _movieRepository.GetMovieByIdAsync(id); 30 | } 31 | 32 | public async Task CreateMovieAsync(Movie movie, IFormFile coverImage) 33 | { 34 | if (coverImage != null) 35 | { 36 | movie.CoverImage = await UploadCoverImageAsync(coverImage); 37 | } 38 | 39 | return await _movieRepository.AddMovieAsync(movie); 40 | } 41 | 42 | public async Task UpdateMovieAsync(Movie movie, IFormFile? coverImage) 43 | { 44 | var existingMovie = await _movieRepository.GetMovieByIdAsync(movie.Id); 45 | if (existingMovie == null) 46 | { 47 | throw new KeyNotFoundException(); 48 | } 49 | 50 | if (coverImage != null && coverImage.Length > 0) 51 | { 52 | // If a new file is provided, delete the old image and save the new one 53 | if (!string.IsNullOrEmpty(existingMovie.CoverImage)) 54 | { 55 | var oldImagePath = Path.Combine(_imageDirectory, existingMovie.CoverImage); 56 | if (File.Exists(oldImagePath)) 57 | { 58 | File.Delete(oldImagePath); 59 | } 60 | } 61 | 62 | movie.CoverImage = await UploadCoverImageAsync(coverImage); 63 | } 64 | else if (string.IsNullOrEmpty(movie.CoverImage)) 65 | { 66 | // If the CoverImage is an empty string, it means the image should be removed 67 | movie.CoverImage = string.Empty; 68 | } 69 | else 70 | { 71 | // Retain the existing image path if it's not removed 72 | movie.CoverImage = existingMovie.CoverImage; 73 | } 74 | 75 | // Update other properties 76 | existingMovie.Title = movie.Title; 77 | existingMovie.Description = movie.Description; 78 | existingMovie.Genre = movie.Genre; 79 | existingMovie.CoverImage = movie.CoverImage; 80 | 81 | await _movieRepository.UpdateMovieAsync(existingMovie); 82 | } 83 | 84 | 85 | public async Task DeleteMovieAsync(int id) 86 | { 87 | var movie = await _movieRepository.GetMovieByIdAsync(id); 88 | if (movie == null) 89 | { 90 | throw new KeyNotFoundException(); 91 | } 92 | 93 | // Optionally, delete the associated image file 94 | if (!string.IsNullOrEmpty(movie.CoverImage)) 95 | { 96 | var imagePath = Path.Combine(_imageDirectory, movie.CoverImage); 97 | if (File.Exists(imagePath)) 98 | { 99 | File.Delete(imagePath); 100 | } 101 | } 102 | 103 | await _movieRepository.DeleteMovieAsync(movie); 104 | } 105 | 106 | public async Task UploadCoverImageAsync(IFormFile coverImage) 107 | { 108 | if (!Directory.Exists(_imageDirectory)) 109 | { 110 | Directory.CreateDirectory(_imageDirectory); 111 | } 112 | 113 | var fileName = $"{Guid.NewGuid()}{Path.GetExtension(coverImage.FileName)}"; 114 | var filePath = Path.Combine(_imageDirectory, fileName); 115 | 116 | using (var stream = new FileStream(filePath, FileMode.Create)) 117 | { 118 | await coverImage.CopyToAsync(stream); 119 | } 120 | 121 | return $"{_imagePathPrefix}{fileName}"; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /MovieAPI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "MovieDbConnection": "Host=db;Database=movie_db;Username=postgres;Password=l^2~e1uJLj*n" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*" 12 | } 13 | -------------------------------------------------------------------------------- /MovieAPI/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | movieapi: 5 | image: movieapi:latest 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | ports: 10 | - "5103:80" 11 | depends_on: 12 | - db 13 | environment: 14 | - ASPNETCORE_ENVIRONMENT=Development 15 | - ConnectionStrings__MovieDbConnection=Host=db;Database=movie_db;Username=postgres;Password=l^2~e1uJLj*n 16 | 17 | db: 18 | image: postgres:15 19 | environment: 20 | POSTGRES_USER: postgres 21 | POSTGRES_PASSWORD: l^2~e1uJLj*n 22 | POSTGRES_DB: movie_db 23 | ports: 24 | - "5432:5432" 25 | volumes: 26 | - postgres_data:/var/lib/postgresql/data 27 | 28 | volumes: 29 | postgres_data: 30 | -------------------------------------------------------------------------------- /MovieAPI/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "MovieAPI": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": true, 7 | "launchUrl": "swagger", 8 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MovieAppUI/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /MovieAppUI/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /MovieAppUI/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /MovieAppUI/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /MovieAppUI/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /MovieAppUI/README.md: -------------------------------------------------------------------------------- 1 | # MovieAppUI 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. 28 | -------------------------------------------------------------------------------- /MovieAppUI/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "MovieAppUI": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": "dist/movie-app-ui", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": [ 24 | "zone.js" 25 | ], 26 | "tsConfig": "tsconfig.app.json", 27 | "inlineStyleLanguage": "scss", 28 | "assets": [ 29 | { 30 | "glob": "**/*", 31 | "input": "public" 32 | }, 33 | "src/assets" 34 | ], 35 | "styles": [ 36 | "@angular/material/prebuilt-themes/cyan-orange.css", 37 | "src/styles.scss" 38 | ], 39 | "scripts": [] 40 | }, 41 | "configurations": { 42 | "production": { 43 | "budgets": [ 44 | { 45 | "type": "initial", 46 | "maximumWarning": "500kB", 47 | "maximumError": "1MB" 48 | }, 49 | { 50 | "type": "anyComponentStyle", 51 | "maximumWarning": "2kB", 52 | "maximumError": "4kB" 53 | } 54 | ], 55 | "outputHashing": "all" 56 | }, 57 | "development": { 58 | "optimization": false, 59 | "extractLicenses": false, 60 | "sourceMap": true 61 | } 62 | }, 63 | "defaultConfiguration": "production" 64 | }, 65 | "serve": { 66 | "builder": "@angular-devkit/build-angular:dev-server", 67 | "configurations": { 68 | "production": { 69 | "buildTarget": "MovieAppUI:build:production" 70 | }, 71 | "development": { 72 | "buildTarget": "MovieAppUI:build:development" 73 | } 74 | }, 75 | "defaultConfiguration": "development" 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n" 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "polyfills": [ 84 | "zone.js", 85 | "zone.js/testing" 86 | ], 87 | "tsConfig": "tsconfig.spec.json", 88 | "inlineStyleLanguage": "scss", 89 | "assets": [ 90 | { 91 | "glob": "**/*", 92 | "input": "public" 93 | } 94 | ], 95 | "styles": [ 96 | "@angular/material/prebuilt-themes/cyan-orange.css", 97 | "src/styles.scss" 98 | ], 99 | "scripts": [] 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "cli": { 106 | "analytics": "23330374-d7b2-4d37-b73a-fb4c6e28bb3c" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /MovieAppUI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-app-ui", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --host 0.0.0.0 --hmr", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^18.2.0", 14 | "@angular/cdk": "^18.2.0", 15 | "@angular/common": "^18.2.0", 16 | "@angular/compiler": "^18.2.0", 17 | "@angular/core": "^18.2.0", 18 | "@angular/forms": "^18.2.0", 19 | "@angular/material": "^18.2.0", 20 | "@angular/platform-browser": "^18.2.0", 21 | "@angular/platform-browser-dynamic": "^18.2.0", 22 | "@angular/router": "^18.2.0", 23 | "rxjs": "~7.8.0", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.14.10" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "^18.2.0", 29 | "@angular/cli": "^18.2.0", 30 | "@angular/compiler-cli": "^18.2.0", 31 | "@types/jasmine": "~5.1.0", 32 | "jasmine-core": "~5.2.0", 33 | "karma": "~6.4.0", 34 | "karma-chrome-launcher": "~3.2.0", 35 | "karma-coverage": "~2.2.0", 36 | "karma-jasmine": "~5.1.0", 37 | "karma-jasmine-html-reporter": "~2.1.0", 38 | "typescript": "~5.5.2" 39 | } 40 | } -------------------------------------------------------------------------------- /MovieAppUI/public/assets/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoft112/movie-list-angular-dotnet/3c283d8ffae8c23bc8b24f4a0c2f400453a9670c/MovieAppUI/public/assets/default.png -------------------------------------------------------------------------------- /MovieAppUI/public/assets/event.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoft112/movie-list-angular-dotnet/3c283d8ffae8c23bc8b24f4a0c2f400453a9670c/MovieAppUI/public/assets/event.jpg -------------------------------------------------------------------------------- /MovieAppUI/public/assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoft112/movie-list-angular-dotnet/3c283d8ffae8c23bc8b24f4a0c2f400453a9670c/MovieAppUI/public/assets/logo.jpg -------------------------------------------------------------------------------- /MovieAppUI/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devsoft112/movie-list-angular-dotnet/3c283d8ffae8c23bc8b24f4a0c2f400453a9670c/MovieAppUI/public/favicon.ico -------------------------------------------------------------------------------- /MovieAppUI/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 40 | Movie Dashboard 41 | 42 | 43 | 44 |
45 | 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .dashboard-container { 2 | height: 100vh; 3 | } 4 | 5 | .sidebar { 6 | background: linear-gradient(145deg, #2c3e50, #34495e); 7 | padding: 20px; 8 | color: #fff; 9 | overflow: hidden; 10 | } 11 | 12 | .sidebar-header { 13 | font-size: 1.5rem; 14 | font-weight: bold; 15 | margin-bottom: 20px; 16 | } 17 | 18 | .nav-item { 19 | display: flex; 20 | align-items: center; 21 | mat-icon { 22 | margin-right: 10px; 23 | } 24 | p { 25 | margin-bottom: 0; 26 | } 27 | } 28 | 29 | .sidebar-footer { 30 | position: absolute; 31 | bottom: 20px; 32 | width: 100%; 33 | text-align: center; 34 | font-size: 0.875rem; 35 | } 36 | 37 | .mat-sidenav-content { 38 | display: flex; 39 | flex-direction: column; 40 | height: 100vh; 41 | overflow-y: auto; 42 | } 43 | 44 | .header-toolbar { 45 | flex-shrink: 0; 46 | justify-content: flex-end; 47 | padding: 0 20px; 48 | height: 64px; 49 | box-shadow: 0 0px 10px rgba(0, 0, 0, 0.8); 50 | } 51 | 52 | .content-area { 53 | flex: 1; 54 | display: flex; 55 | flex-direction: column; 56 | } 57 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'MovieAppUI' title`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('MovieAppUI'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, MovieAppUI'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 3 | import { CommonModule } from '@angular/common'; 4 | import { RouterLink, RouterOutlet } from '@angular/router'; 5 | 6 | import { MatToolbarModule } from '@angular/material/toolbar'; 7 | import { MatSidenavModule } from '@angular/material/sidenav'; 8 | import { MatListModule } from '@angular/material/list'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | 12 | @Component({ 13 | selector: 'app-root', 14 | standalone: true, 15 | imports: [ 16 | CommonModule, 17 | RouterLink, 18 | RouterOutlet, 19 | MatToolbarModule, 20 | MatSidenavModule, 21 | MatListModule, 22 | MatIconModule, 23 | MatButtonModule,], 24 | templateUrl: './app.component.html', 25 | styleUrl: './app.component.scss' 26 | }) 27 | export class AppComponent { 28 | title = 'MovieAppUI'; 29 | 30 | isSmallScreen = false; 31 | 32 | constructor(private breakpointObserver: BreakpointObserver) { 33 | this.breakpointObserver.observe([Breakpoints.Handset, Breakpoints.Tablet]).subscribe(result => { 34 | this.isSmallScreen = result.matches; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | // app.config.ts 2 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 3 | import { provideRouter } from '@angular/router'; 4 | import { provideHttpClient } from '@angular/common/http'; 5 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 6 | 7 | import { routes } from './app.routes'; 8 | 9 | export const appConfig: ApplicationConfig = { 10 | providers: [ 11 | provideZoneChangeDetection({ eventCoalescing: true }), 12 | provideRouter(routes), 13 | provideHttpClient(), 14 | provideAnimationsAsync() 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { MoviesListComponent } from './pages/movies-list/movies-list.component'; 3 | import { CreateMovieComponent } from './pages/create-movie/create-movie.component'; 4 | import { MovieDetailComponent } from './pages/movie-detail/movie-detail.component'; 5 | 6 | export const routes: Routes = [ 7 | { path: '', component: MoviesListComponent }, 8 | { path: 'create-movie', component: CreateMovieComponent }, 9 | { path: 'edit-movie/:id', component: CreateMovieComponent }, 10 | { path: 'details-movie/:id', component: MovieDetailComponent } 11 | ]; 12 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/confirm-dialog/confirm-dialog.component.html: -------------------------------------------------------------------------------- 1 |

Confirm Deletion

2 | 3 |

{{ data.message }}

4 |
5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/confirm-dialog/confirm-dialog.component.scss: -------------------------------------------------------------------------------- 1 | mat-dialog-content { 2 | font-size: 16px; 3 | color: #333; 4 | } 5 | 6 | mat-dialog-actions { 7 | margin-top: 20px; 8 | } 9 | 10 | .dialog-cancel-button { 11 | margin-right: 10px; 12 | } 13 | 14 | .mat-dialog-title { 15 | font-size: 24px; 16 | font-weight: bold; 17 | color: #333; 18 | } 19 | 20 | .mat-dialog-container { 21 | border-radius: 8px; 22 | padding: 20px; 23 | background-color: #fff; 24 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 25 | } 26 | 27 | button { 28 | text-transform: none; 29 | } 30 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/confirm-dialog/confirm-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ConfirmDialogComponent } from './confirm-dialog.component'; 4 | 5 | describe('ConfirmDialogComponent', () => { 6 | let component: ConfirmDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ConfirmDialogComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ConfirmDialogComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/confirm-dialog/confirm-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { MatDialogModule } from '@angular/material/dialog'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { CommonModule } from '@angular/common'; 6 | 7 | @Component({ 8 | selector: 'app-confirm-dialog', 9 | templateUrl: './confirm-dialog.component.html', 10 | styleUrls: ['./confirm-dialog.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | CommonModule, 14 | MatDialogModule, // Import MatDialogModule 15 | MatButtonModule // Import MatButtonModule 16 | ] 17 | }) 18 | export class ConfirmDialogComponent { 19 | constructor( 20 | public dialogRef: MatDialogRef, 21 | @Inject(MAT_DIALOG_DATA) public data: any 22 | ) {} 23 | 24 | onConfirm(): void { 25 | this.dialogRef.close(true); 26 | } 27 | 28 | onCancel(): void { 29 | this.dialogRef.close(false); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/movie-card/movie-card.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ movie.title }} 8 | 9 | 10 |
{{ movie.title }}
11 |

{{ getShortDescription(movie.description) }}

12 |
13 | 14 | {{ genre }} 15 | 16 | more... 17 |
18 |
19 | 20 | 21 | 28 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/movie-card/movie-card.component.scss: -------------------------------------------------------------------------------- 1 | .movie-card { 2 | min-width: 320px; 3 | margin: 16px; 4 | padding: 16px; 5 | border-radius: 12px; 6 | background-color: #2c2c2c; 7 | color: #ffffff; 8 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6); 9 | transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; 10 | cursor: pointer; 11 | 12 | &:hover { 13 | transform: translateY(-8px); 14 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.8); 15 | } 16 | 17 | img { 18 | height: 180px; 19 | object-fit: cover; 20 | border-radius: 8px; 21 | } 22 | 23 | h5 { 24 | font-size: 1.5rem; 25 | margin-bottom: 10px; 26 | height: 4.2rem; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | display: -webkit-box; 30 | -webkit-line-clamp: 2; 31 | -webkit-box-orient: vertical; 32 | color: #ffffff; 33 | } 34 | 35 | p { 36 | font-size: 1rem; 37 | color: #d3d3d3; 38 | margin-bottom: 10px; 39 | min-height: 96px; 40 | } 41 | 42 | .mat-chip-list { 43 | margin-bottom: 10px; 44 | display: flex; 45 | flex-wrap: wrap; 46 | align-items: center; 47 | 48 | mat-chip { 49 | margin-right: 4px; 50 | background-color: #424242; 51 | color: #ffca28; 52 | } 53 | } 54 | 55 | .mat-card-actions { 56 | display: flex; 57 | justify-content: space-between; 58 | padding: 0; 59 | margin-top: auto; 60 | width: 100%; 61 | 62 | button { 63 | text-transform: uppercase; 64 | margin-right: 10px; 65 | flex: 1; 66 | max-width: 45%; 67 | color: #ffffff; 68 | } 69 | 70 | button[color="primary"] { 71 | background-color: #00796b; 72 | } 73 | 74 | button[color="warn"] { 75 | background-color: #d32f2f; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/movie-card/movie-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MovieCardComponent } from './movie-card.component'; 4 | 5 | describe('MovieCardComponent', () => { 6 | let component: MovieCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [MovieCardComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MovieCardComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/movie-card/movie-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatChipsModule } from '@angular/material/chips'; 6 | import { CommonModule } from '@angular/common'; 7 | 8 | import { environment } from '../../../environments/environment'; 9 | 10 | @Component({ 11 | selector: 'app-movie-card', 12 | templateUrl: './movie-card.component.html', 13 | styleUrls: ['./movie-card.component.scss'], 14 | standalone: true, 15 | imports: [ 16 | CommonModule, 17 | MatCardModule, 18 | MatButtonModule, 19 | MatChipsModule 20 | ] 21 | }) 22 | export class MovieCardComponent { 23 | @Input() movie: any; 24 | @Output() delete = new EventEmitter(); 25 | 26 | baseURL = environment.baseURL; 27 | defaultImage = './assets/default.png'; 28 | 29 | constructor(private router: Router) { } 30 | 31 | getDisplayedGenres(genres: string[]): string[] { 32 | return genres.slice(0, 2); // Return only the first two genres 33 | } 34 | 35 | getImageSrc(imageUrl: string): string { 36 | return `${this.baseURL}${imageUrl}`; 37 | } 38 | 39 | onImageError(event: Event) { 40 | const imgElement = event.target as HTMLImageElement; 41 | imgElement.src = this.defaultImage; 42 | } 43 | 44 | getShortDescription(description: string): string { 45 | const maxLength = 120; // Maximum characters before truncation 46 | return description.length > maxLength ? description.slice(0, maxLength) + '...' : description; 47 | } 48 | 49 | navigateToDetail(movieId: number): void { 50 | this.router.navigate(['/details-movie', movieId]); 51 | } 52 | 53 | editMovie(movieId: number, event: Event): void { 54 | event.stopPropagation(); // Prevent card click from triggering 55 | this.router.navigate(['/edit-movie', movieId]); 56 | } 57 | 58 | deleteMovie(movieId: number, event: Event): void { 59 | event.stopPropagation(); 60 | this.delete.emit(movieId); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/skeleton-movie-card/skeleton-movie-card.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |

6 |
7 | 8 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/skeleton-movie-card/skeleton-movie-card.component.scss: -------------------------------------------------------------------------------- 1 | .skeleton-movie-card { 2 | width: 100%; 3 | height: 350px; 4 | border-radius: 8px; 5 | background-color: #333333; 6 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 7 | margin-bottom: 20px; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | } 12 | 13 | .skeleton-image { 14 | height: 180px; 15 | background-color: #444444; 16 | } 17 | 18 | .skeleton-title, 19 | .skeleton-text { 20 | background-color: #555555; 21 | border-radius: 4px; 22 | margin: 10px 0; 23 | } 24 | 25 | .skeleton-title { 26 | height: 20px; 27 | width: 70%; 28 | } 29 | 30 | .skeleton-text { 31 | height: 15px; 32 | width: 100%; 33 | } 34 | 35 | .skeleton-chips { 36 | display: flex; 37 | gap: 8px; 38 | margin-top: 10px; 39 | } 40 | 41 | .skeleton-chip { 42 | width: 60px; 43 | height: 20px; 44 | background-color: #555555; 45 | border-radius: 12px; 46 | } -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/skeleton-movie-card/skeleton-movie-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SkeletonMovieCardComponent } from './skeleton-movie-card.component'; 4 | 5 | describe('SkeletonMovieCardComponent', () => { 6 | let component: SkeletonMovieCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SkeletonMovieCardComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SkeletonMovieCardComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/components/skeleton-movie-card/skeleton-movie-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | 5 | @Component({ 6 | selector: 'app-skeleton-movie-card', 7 | templateUrl: './skeleton-movie-card.component.html', 8 | styleUrls: ['./skeleton-movie-card.component.scss'], 9 | standalone: true, 10 | imports: [CommonModule, MatCardModule] 11 | }) 12 | export class SkeletonMovieCardComponent { } 13 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/create-movie/create-movie.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Title 4 | 9 | movie 10 | Enter a unique movie title 11 | 12 | 13 | 14 | Genres 15 | 16 | 17 | {{ genre }} 18 | 19 | 20 | local_offer 21 | Select one or more genres 22 | 23 | 24 | 25 | Description 26 | 32 | description 33 | Provide a brief summary of the movie 34 | 35 | 36 |
45 |

Drag and drop your cover image here, or click to select a file.

46 | 52 |
53 | 54 |
55 | Cover Image Preview 61 | 64 |
65 | 66 | 74 |
75 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/create-movie/create-movie.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | 5 | .movie-form { 6 | max-width: 600px; 7 | margin: 0 auto; 8 | margin-top: 40px; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 20px; 12 | 13 | .file-drop-zone { 14 | border: 2px dashed #cccccc; 15 | border-radius: 8px; 16 | padding: 20px; 17 | text-align: center; 18 | cursor: pointer; 19 | transition: background-color 0.3s ease; 20 | background-color: #f9f9f9; 21 | 22 | p { 23 | margin: 0; 24 | font-size: 1rem; 25 | color: #666666; 26 | } 27 | 28 | input[type='file'] { 29 | display: none; 30 | } 31 | 32 | &.drop-over { 33 | background-color: #e0e0e0; 34 | } 35 | } 36 | 37 | .image-preview-wrapper { 38 | text-align: center; 39 | 40 | .image-preview { 41 | max-width: 100%; 42 | margin-top: 10px; 43 | border-radius: 4px; 44 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 45 | } 46 | 47 | button { 48 | margin-top: 10px; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/create-movie/create-movie.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateMovieComponent } from './create-movie.component'; 4 | 5 | describe('CreateMovieComponent', () => { 6 | let component: CreateMovieComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CreateMovieComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CreateMovieComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/create-movie/create-movie.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; 4 | import { CommonModule } from '@angular/common'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | import { MatSelectModule } from '@angular/material/select'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatSnackBar } from '@angular/material/snack-bar'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | 11 | import { MovieService } from '../../services/movie.service'; 12 | import { environment } from '../../../environments/environment'; 13 | 14 | @Component({ 15 | selector: 'app-create-movie', 16 | standalone: true, 17 | imports: [CommonModule, ReactiveFormsModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule], 18 | templateUrl: './create-movie.component.html', 19 | styleUrls: ['./create-movie.component.scss'] 20 | }) 21 | export class CreateMovieComponent implements OnInit { 22 | movieForm: FormGroup; 23 | genres: string[] = ['Action', 'Drama', 'Comedy', 'Sci-Fi', 'Romance', 'Thriller', 'Horror', 'Animation', 'Adventure', 'Fantasy']; 24 | coverImagePreview: string | ArrayBuffer | null = null; 25 | isEditMode = false; 26 | movieId: number | null = null; 27 | isDragOver = false; 28 | removeImageFlag = false; 29 | baseURL = environment.baseURL; 30 | defaultImage = './assets/default.png'; 31 | 32 | @ViewChild('fileInput', { static: false }) fileInput!: ElementRef; 33 | 34 | constructor( 35 | private fb: FormBuilder, 36 | private movieService: MovieService, 37 | private route: ActivatedRoute, 38 | private router: Router, 39 | private snackBar: MatSnackBar 40 | ) { 41 | this.movieForm = this.fb.group({ 42 | title: ['', Validators.required], 43 | description: ['', Validators.required], 44 | genre: [[], Validators.required], 45 | coverImage: [null] 46 | }); 47 | } 48 | 49 | ngOnInit(): void { 50 | this.route.paramMap.subscribe(params => { 51 | this.movieId = +params.get('id')!; 52 | this.isEditMode = !!this.movieId; 53 | 54 | if (this.isEditMode) { 55 | this.loadMovieDetails(this.movieId); 56 | } 57 | }); 58 | } 59 | 60 | loadMovieDetails(id: number): void { 61 | this.movieService.getMovieById(id).subscribe(movie => { 62 | this.movieForm.patchValue({ 63 | title: movie.title, 64 | description: movie.description, 65 | genre: movie.genre, 66 | coverImage: movie.coverImage ? movie.coverImage : null 67 | }); 68 | this.coverImagePreview = movie.coverImage ? this.baseURL + movie.coverImage : this.defaultImage; 69 | }); 70 | } 71 | 72 | 73 | onCoverImageChange(event: Event): void { 74 | const file = (event.target as HTMLInputElement).files?.[0]; 75 | if (file) { 76 | this.movieForm.patchValue({ coverImage: file }); 77 | const reader = new FileReader(); 78 | reader.onload = () => { 79 | this.coverImagePreview = reader.result; 80 | }; 81 | reader.readAsDataURL(file); 82 | this.removeImageFlag = false; 83 | } 84 | } 85 | 86 | onDragOver(event: DragEvent): void { 87 | event.preventDefault(); 88 | this.isDragOver = true; 89 | } 90 | 91 | onDragLeave(event: DragEvent): void { 92 | event.preventDefault(); 93 | this.isDragOver = false; 94 | } 95 | 96 | onDrop(event: DragEvent): void { 97 | event.preventDefault(); 98 | this.isDragOver = false; 99 | 100 | const file = event.dataTransfer?.files[0]; 101 | if (file) { 102 | this.movieForm.patchValue({ coverImage: file }); 103 | const reader = new FileReader(); 104 | reader.onload = () => { 105 | this.coverImagePreview = reader.result; 106 | }; 107 | reader.readAsDataURL(file); 108 | this.removeImageFlag = false; 109 | } 110 | } 111 | 112 | triggerFileInput(): void { 113 | if (this.fileInput) { 114 | this.fileInput.nativeElement.click(); 115 | } 116 | } 117 | 118 | onImageError(event: Event) { 119 | const imgElement = event.target as HTMLImageElement; 120 | imgElement.src = this.defaultImage; 121 | } 122 | 123 | removeImage(event: Event): void { 124 | event.preventDefault(); 125 | event.stopPropagation(); 126 | this.coverImagePreview = null; 127 | this.movieForm.patchValue({ coverImage: null }); 128 | this.removeImageFlag = true; 129 | } 130 | 131 | submitForm(): void { 132 | if (this.movieForm.valid) { 133 | const formData = new FormData(); 134 | 135 | if (this.isEditMode && this.movieId !== null) { 136 | formData.append('id', this.movieId.toString()); 137 | } 138 | 139 | formData.append('title', this.movieForm.get('title')?.value); 140 | formData.append('description', this.movieForm.get('description')?.value); 141 | 142 | const genreString = this.movieForm.get('genre')?.value.join(','); 143 | formData.append('genre', genreString); 144 | 145 | const coverImage = this.movieForm.get('coverImage')?.value; 146 | 147 | if (coverImage instanceof File) { 148 | formData.append('coverImage', coverImage); 149 | } else if (this.removeImageFlag) { 150 | formData.append('coverImage', ''); // Send an empty string to indicate removal 151 | } else if (!coverImage) { 152 | formData.append('coverImage', this.movieForm.get('coverImage')?.value || ''); // Handle cases where no image change 153 | } else { 154 | formData.append('coverImage', coverImage); 155 | } 156 | 157 | if (this.isEditMode && this.movieId !== null) { 158 | this.movieService.updateMovie(this.movieId, formData).subscribe(response => { 159 | this.snackBar.open('Movie updated successfully!', 'Close', { duration: 3000 }); 160 | this.router.navigate(['/']); // Navigate back to the list 161 | }, error => { 162 | this.snackBar.open('Failed to update movie.', 'Close', { duration: 3000 }); 163 | }); 164 | } else { 165 | this.movieService.createMovie(formData).subscribe(response => { 166 | this.snackBar.open('Movie created successfully!', 'Close', { duration: 3000 }); 167 | this.router.navigate(['/']); // Navigate back to the list 168 | }, error => { 169 | this.snackBar.open('Failed to create movie.', 'Close', { duration: 3000 }); 170 | }); 171 | } 172 | } 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movie-detail/movie-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ movie?.title }} 4 | 5 |
6 | {{ genre }} 7 |
8 |
9 |
10 | {{ movie?.title }} 16 | 17 |

{{ movie?.description }}

18 |
19 | 20 | 21 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movie-detail/movie-detail.component.scss: -------------------------------------------------------------------------------- 1 | .movie-detail-card { 2 | max-width: 800px; 3 | margin: 40px auto; 4 | padding: 20px; 5 | background-color: #2c2c2c; 6 | color: #ffffff; 7 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.6); 8 | border-radius: 12px; 9 | 10 | mat-card-title { 11 | font-size: 2rem; 12 | font-weight: bold; 13 | } 14 | 15 | mat-card-subtitle { 16 | font-size: 1.2rem; 17 | margin-bottom: 10px; 18 | 19 | mat-chip { 20 | background-color: #424242; 21 | color: #ffca28; 22 | } 23 | } 24 | 25 | .mat-chip-list { 26 | margin: 20px 0; 27 | 28 | mat-chip { 29 | margin-right: 15px; 30 | } 31 | } 32 | 33 | img { 34 | height: 400px; 35 | object-fit: cover; 36 | border-radius: 12px; 37 | margin-bottom: 20px; 38 | } 39 | 40 | mat-card-content { 41 | font-size: 1.2rem; 42 | line-height: 1.6; 43 | } 44 | 45 | mat-card-actions { 46 | display: flex; 47 | justify-content: space-between; 48 | } 49 | } -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movie-detail/movie-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MovieDetailComponent } from './movie-detail.component'; 4 | 5 | describe('MovieDetailComponent', () => { 6 | let component: MovieDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [MovieDetailComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MovieDetailComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movie-detail/movie-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { MovieService } from '../../services/movie.service'; 4 | import { CommonModule } from '@angular/common'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatChipsModule } from '@angular/material/chips'; 9 | 10 | import { environment } from '../../../environments/environment'; 11 | 12 | @Component({ 13 | selector: 'app-movie-detail', 14 | templateUrl: './movie-detail.component.html', 15 | styleUrls: ['./movie-detail.component.scss'], 16 | standalone: true, 17 | imports: [CommonModule, MatButtonModule, MatCardModule, MatIconModule, MatChipsModule], 18 | }) 19 | export class MovieDetailComponent implements OnInit { 20 | movie: any; 21 | baseURL = environment.baseURL; 22 | defaultImage = './assets/default.png'; 23 | 24 | constructor(private route: ActivatedRoute, private router: Router, private movieService: MovieService) { } 25 | 26 | ngOnInit(): void { 27 | const id = +this.route.snapshot.paramMap.get('id')!; 28 | this.movieService.getMovieById(id).subscribe((movie) => { 29 | this.movie = movie; 30 | }); 31 | } 32 | 33 | getImageSrc(imageUrl: string): string { 34 | return `${this.baseURL}${imageUrl}`; 35 | } 36 | 37 | onImageError(event: Event) { 38 | const imgElement = event.target as HTMLImageElement; 39 | imgElement.src = this.defaultImage; 40 | } 41 | 42 | editMovie(movieId: number, event: Event): void { 43 | event.stopPropagation(); // Prevent card click from triggering 44 | this.router.navigate(['/edit-movie', movieId]); 45 | } 46 | 47 | navigateToList(): void { 48 | this.router.navigate(['/']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movies-list/movies-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 7 | 8 | 9 | 10 |
11 | error_outline 12 |

{{ errorMessage }}

13 |
14 |
15 | 16 | 17 | 22 | 23 | 24 |
25 | movie 26 |

No movies available. Please add some movies.

27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | 42 |
43 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movies-list/movies-list.component.scss: -------------------------------------------------------------------------------- 1 | .movies-list-container { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | height: calc(100vh - 64px); 6 | } 7 | 8 | .movies-list { 9 | flex: 1; 10 | display: grid; 11 | grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); 12 | grid-gap: 20px; 13 | padding: 20px; 14 | overflow-y: auto; 15 | } 16 | 17 | .error-message { 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | transform: translate(-50%, -50%); 22 | } 23 | 24 | .error-message, 25 | .no-data-message { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | justify-content: center; 30 | height: 100%; 31 | color: #ffffff; 32 | text-align: center; 33 | } 34 | 35 | .error-message mat-icon, 36 | .no-data-message mat-icon { 37 | font-size: 48px; 38 | margin-bottom: 20px; 39 | color: #ff6b6b; /* Use an appropriate color for errors */ 40 | } 41 | 42 | .error-message p, 43 | .no-data-message p { 44 | font-size: 18px; 45 | color: #ffffff; 46 | } 47 | 48 | mat-paginator { 49 | position: sticky; 50 | bottom: 0; 51 | background-color: #2c2c2c; 52 | color: #ffffff; 53 | padding: 10px 0; 54 | box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.6); 55 | z-index: 10; 56 | 57 | .mat-paginator-range-label, 58 | .mat-paginator-page-size-label, 59 | .mat-paginator-range-actions button { 60 | color: #ffffff; 61 | } 62 | 63 | .mat-paginator-icon { 64 | color: #ffffff; 65 | } 66 | 67 | .mat-select-value-text { 68 | color: #ffca28; 69 | } 70 | 71 | .mat-paginator-page-size-options { 72 | .mat-option { 73 | background-color: #424242; 74 | color: #ffffff; 75 | } 76 | } 77 | 78 | .mat-paginator-navigation-button { 79 | background-color: #424242; 80 | color: #ffca28; 81 | } 82 | 83 | .mat-paginator-navigation-button[disabled] { 84 | color: #757575; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movies-list/movies-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MoviesListComponent } from './movies-list.component'; 4 | 5 | describe('MoviesListComponent', () => { 6 | let component: MoviesListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [MoviesListComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MoviesListComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/pages/movies-list/movies-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatButtonModule } from '@angular/material/button'; 7 | import { MatPaginatorModule } from '@angular/material/paginator'; 8 | import { MatDialog, MatDialogModule } from '@angular/material/dialog'; 9 | import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; 10 | 11 | import { MovieService } from '../../services/movie.service'; 12 | import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component'; 13 | import { MovieCardComponent } from '../../components/movie-card/movie-card.component'; 14 | import { SkeletonMovieCardComponent } from '../../components/skeleton-movie-card/skeleton-movie-card.component'; 15 | 16 | @Component({ 17 | selector: 'app-movies-list', 18 | templateUrl: './movies-list.component.html', 19 | styleUrls: ['./movies-list.component.scss'], 20 | standalone: true, 21 | imports: [ 22 | CommonModule, 23 | RouterModule, 24 | MatCardModule, 25 | MatButtonModule, 26 | MatPaginatorModule, 27 | MatDialogModule, 28 | MatSnackBarModule, 29 | MovieCardComponent, 30 | MatIconModule, 31 | SkeletonMovieCardComponent 32 | ] 33 | }) 34 | export class MoviesListComponent implements OnInit { 35 | movies: any[] = []; 36 | loading: boolean = true; 37 | errorMessage: string | null = null; 38 | skeletonArray = Array(10); 39 | 40 | pageSize: number = 10; 41 | pageNumber: number = 1; 42 | totalMovies: number = 0; 43 | 44 | constructor(private movieService: MovieService, private snackBar: MatSnackBar, private dialog: MatDialog) { } 45 | 46 | ngOnInit(): void { 47 | this.fetchMovies(); 48 | } 49 | 50 | fetchMovies(): void { 51 | this.loading = true; 52 | this.errorMessage = null; 53 | this.movieService.getMovies(this.pageNumber, this.pageSize).subscribe( 54 | (data) => { 55 | this.movies = data.items; 56 | this.totalMovies = data.totalCount; 57 | this.loading = false; 58 | if (this.movies.length === 0) { 59 | this.errorMessage = 'No movies available.'; 60 | } 61 | }, 62 | (error) => { 63 | this.loading = false; 64 | this.errorMessage = 'Failed to load movies. Please try again later.'; 65 | } 66 | ); 67 | } 68 | 69 | onPageChange(event: any): void { 70 | this.pageNumber = event.pageIndex + 1; 71 | this.pageSize = event.pageSize; 72 | this.fetchMovies(); 73 | } 74 | 75 | deleteMovie(movieId: number): void { 76 | const dialogRef = this.dialog.open(ConfirmDialogComponent, { 77 | width: '550px', 78 | data: { message: 'Are you sure you want to delete this movie?' } 79 | }); 80 | 81 | dialogRef.afterClosed().subscribe((result) => { 82 | if (result) { 83 | this.movieService.deleteMovie(movieId).subscribe( 84 | () => { 85 | this.snackBar.open('Movie deleted successfully!', 'Close', { duration: 3000 }); 86 | this.fetchMovies(); // Refresh the movie list 87 | }, 88 | (error) => { 89 | this.snackBar.open('Failed to delete movie.', 'Close', { duration: 3000 }); 90 | } 91 | ); 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/services/movie.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { MovieService } from './movie.service'; 4 | 5 | describe('MovieService', () => { 6 | let service: MovieService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(MovieService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /MovieAppUI/src/app/services/movie.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '../../environments/environment'; 6 | 7 | interface PaginatedMoviesResponse { 8 | items: any[]; 9 | totalCount: number; 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class MovieService { 16 | private apiUrl = `${environment.baseURL}/api/movies`; 17 | 18 | constructor(private http: HttpClient) { } 19 | 20 | createMovie(movieData: FormData): Observable { 21 | return this.http.post(this.apiUrl, movieData); 22 | } 23 | 24 | getMovies(pageNumber: number = 1, pageSize: number = 10): Observable { 25 | return this.http.get(this.apiUrl, { 26 | params: { 27 | pageNumber: pageNumber.toString(), 28 | pageSize: pageSize.toString(), 29 | }, 30 | }); 31 | } 32 | 33 | getMovieById(id: number): Observable { 34 | return this.http.get(`${this.apiUrl}/${id}`); 35 | } 36 | 37 | updateMovie(id: number, movieData: FormData): Observable { 38 | return this.http.put(`${this.apiUrl}/${id}`, movieData); 39 | } 40 | 41 | deleteMovie(id: number): Observable { 42 | return this.http.delete(`${this.apiUrl}/${id}`); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MovieAppUI/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | baseURL: 'https://sp-space-prod.com' 4 | }; -------------------------------------------------------------------------------- /MovieAppUI/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | baseURL: 'http://localhost:5103' 4 | }; -------------------------------------------------------------------------------- /MovieAppUI/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MovieAppUI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /MovieAppUI/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /MovieAppUI/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | font-family: Roboto, "Helvetica Neue", sans-serif; 11 | } 12 | 13 | .custom-dialog-container { 14 | border-radius: 8px; 15 | padding: 0; 16 | } 17 | 18 | .mat-dialog-container { 19 | background: #f5f5f5; 20 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); 21 | } 22 | 23 | 24 | ::ng-deep .mat-dialog-container { 25 | background: linear-gradient(145deg, #2c3e50, #34495e); 26 | /* Same as sidebar background */ 27 | color: #ffffff; 28 | /* Text color for better contrast */ 29 | } 30 | 31 | ::ng-deep .mat-dialog-title { 32 | color: #ffffff; 33 | /* Ensure title text is visible */ 34 | } 35 | 36 | ::ng-deep .mat-dialog-content { 37 | color: #e0e0e0; 38 | /* Ensure content text is visible */ 39 | } 40 | 41 | ::ng-deep .mat-dialog-actions .mat-button { 42 | color: #ffca28; 43 | /* Accent color for buttons */ 44 | } -------------------------------------------------------------------------------- /MovieAppUI/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /MovieAppUI/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "bundler", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MovieAppUI/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Movie List Application 2 | 3 | This application is a full-stack movie viewer developed with Angular 18 and .NET 8. It enables users to view, create, update, and delete movies, each with a title, description, genre, and cover image. The back-end API manages movie data stored in an SQL database. 4 | 5 | ## Features 6 | 7 | ### Front-End 8 | 9 | - **Movie List**: Displays all movies in a paginated format. 10 | - **Movie Detail View**: Shows detailed information about each movie. 11 | - **CRUD Operations**: Allows users to create, update, and delete movies. 12 | - **Drag-and-Drop Image Upload**: Users can set a movie's cover image by dragging and dropping an image file. 13 | - **Responsive Design**: Optimized for a wide range of devices. 14 | - **Loading Indicators**: Displayed while data is being fetched or submitted. 15 | - **Error Messages**: Handles and displays error messages for validation failures and other issues. 16 | 17 | ### Back-End 18 | 19 | - **RESTful API**: Manages movies with GET, POST, PUT, and DELETE operations. 20 | - **Image Handling**: Supports uploading and managing movie cover images. 21 | - **Sorting and Pagination**: Allows sorting of movies by Id and supports pagination. 22 | - **Swagger Integration**: Provides interactive API documentation. 23 | - **Data Seeding**: Seeds the database with initial movie data. 24 | - **CORS Configuration**: Allows cross-origin requests from the front-end application. 25 | 26 | ## Project Structure 27 | 28 | The project is divided into two main parts: 29 | 30 | - **MovieAPI (Back-End)** 31 | - Framework: C# .NET 8 32 | - Functionality: RESTful API, Entity Framework Core 8 for database operations, Docker support, Swagger documentation, sorting and pagination support. 33 | - **MovieAppUI (Front-End)** 34 | - Framework: Angular 18 35 | - Functionality: Displays list of movies, provides forms for movie creation and editing, uses Angular Material for UI components and styling, supports search and filtering. 36 | 37 | ## Technologies Used 38 | 39 | - .NET 8.0 40 | - Angular 18 41 | - Entity Framework Core (PostgreSQL) 42 | - Swashbuckle (Swagger) 43 | - Docker 44 | - PostgreSQL 45 | 46 | ## Setup Instructions 47 | 48 | ### Prerequisites 49 | 50 | - .NET 8 SDK 51 | - Node.js 52 | - Docker (Optional) 53 | - PostgreSQL (Optional) 54 | 55 | ### Option 1: Running with Docker for .NET Backend 56 | 57 | 1. Navigate to the root directory of .net backend: `cd sb-space-angular18-dotnet8\MovieAPI` 58 | 2. Build and run the Docker containers: `docker-compose up --build -d` 59 | 3. Access the API at http://localhost:5103 and Swagger UI at http://localhost:5103/index.html 60 | 61 | ### Option 2: Running Without Docker 62 | 63 | #### Back-End (MovieAPI) 64 | 65 | 1. Set Up PostgreSQL: Ensure PostgreSQL is installed and running. Create a database named `movie_db`. 66 | 2. Update the Connection String in `appsettings.json`. 67 | Modify the `ConnectionStrings.MovieDbConnection` in `appsettings.Development.json`: 68 | 69 | ```json 70 | "ConnectionStrings": { 71 | "MovieDbConnection": "Host=localhost;Database=movie_db;Username=[username];Password=[password]" 72 | } 73 | ``` 74 | 3. Run the Migrations: `dotnet ef database update` 75 | 4. Run the API: `dotnet run` 76 | 5. Access the API at http://localhost:5103 and Swagger UI at http://localhost:5103/index.html 77 | 78 | #### Front-End (MovieAppUI) 79 | 80 | 1. Navigate to the MovieAppUI directory: `cd sb-space-angular18-dotnet8/MovieAppUI` 81 | 2. Install npm packages: `npm install` 82 | 3. Run the Angular application: `ng serve` 83 | 4. The application should be running on http://localhost:4200. 84 | 85 | ## API Endpoints 86 | 87 | - GET /api/movies: Retrieves a paginated list of movies, sorted by Id. 88 | - GET /api/movies/{id}: Retrieves a specific movie by ID. 89 | - POST /api/movies: Creates a new movie. 90 | - PUT /api/movies/{id}: Updates an existing movie. 91 | - DELETE /api/movies/{id}: Deletes a movie by ID. 92 | 93 | ## Swagger Documentation 94 | 95 | Swagger is integrated into this project for API documentation and testing. Access Swagger UI at http://localhost:5103/index.html. 96 | 97 | ## Database Seeding 98 | 99 | The application seeds the database with initial data on startup if the database is empty. To modify the seed data, edit the `MovieSeedData.cs` file in the `Data/Seeding` folder. 100 | 101 | ## Assumptions and Design Decisions 102 | 103 | - **Data Seeding**: The database is seeded with initial movies to simplify testing and validation. 104 | - **Error Handling**: Basic error handling is implemented for both API and UI to manage common scenarios. 105 | - **Validation**: Form validation is implemented on the front-end to ensure that all required fields are filled before submission. --------------------------------------------------------------------------------